Может быть volatile в пользовательских типах, чтобы помочь написанию многопоточного кода - PullRequest
25 голосов
/ 22 марта 2010

Я знаю, что в нескольких вопросах / ответах ранее было совершенно ясно, что volatile относится к видимому состоянию модели памяти c ++, а не к многопоточности.

С другой стороны, эта статья Александреску использует ключевое слово volatile не как функцию времени выполнения, а скорее как проверку времени компиляции, чтобы заставить компилятор не принимать код, который может не быть потоком безопасный. В статье ключевое слово используется скорее как тег required_thread_safety, чем фактическое предполагаемое использование volatile.

Уместно ли это (ab) использование volatile? Какие возможные ошибки могут быть скрыты в подходе?

Первое, что приходит на ум, - это дополнительная путаница: volatile не связан с безопасностью потоков, но из-за отсутствия лучшего инструмента я мог бы его принять.

Основное упрощение статьи:

Если вы объявите переменную volatile, к ней можно будет вызывать только методы-члены volatile, поэтому компилятор будет блокировать вызывающий код для других методов. Объявление экземпляра std::vector как volatile заблокирует все виды использования класса. При добавлении оболочки в форме указателя блокировки, выполняющего const_cast для освобождения требования volatile, любой доступ через указатель блокировки будет разрешен.

Кража из артикула:

template <typename T>
class LockingPtr {
public:
   // Constructors/destructors
   LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx)
   { mtx.Lock(); }
   ~LockingPtr()   { pMtx_->Unlock(); }
   // Pointer behavior
   T& operator*()  { return *pObj_; }
   T* operator->() { return pObj_; }
private:
   T* pObj_;
   Mutex* pMtx_;
   LockingPtr(const LockingPtr&);
   LockingPtr& operator=(const LockingPtr&);
};

class SyncBuf {
public:
   void Thread1() {
      LockingPtr<BufT> lpBuf(buffer_, mtx_);
      BufT::iterator i = lpBuf->begin();
      for (; i != lpBuf->end(); ++i) {
         // ... use *i ...
      }
   }
   void Thread2();
private:
   typedef vector<char> BufT;
   volatile BufT buffer_;
   Mutex mtx_; // controls access to buffer_
};

Примечание

После того, как появились первые пару ответов, я думаю, что должен уточнить, так как, возможно, я не использовал самые подходящие слова.

Использование volatile не из-за того, что он предоставляет во время выполнения, а из-за того, что он означает во время компиляции. То есть тот же трюк можно использовать с ключевым словом const, если оно редко используется в пользовательских типах, как volatile. То есть, есть ключевое слово (которое пишется как volatile), которое позволяет мне блокировать вызовы функций-членов, и Александреску использует его, чтобы обмануть компилятор в том, что он не может скомпилировать небезопасный код.

Я вижу в этом много уловок метапрограммирования, которые существуют не из-за того, что они делают во время компиляции, а из-за того, что заставляет компилятор делать для вас.

Ответы [ 8 ]

6 голосов
/ 22 марта 2010

Я думаю, что проблема не в безопасности потоков, обеспечиваемой volatile. Это не так, и в статье Андрея не говорится, что это так. Здесь для достижения этого используется mutex. Проблема в том, является ли использование ключевого слова volatile для обеспечения статической проверки типов наряду с использованием мьютекса для поточно-безопасного кода нарушением ключевого слова volatile? ИМХО, это довольно умно, но я встречал разработчиков, которые не являются поклонниками строгой проверки типов просто ради этого.

IMO, когда вы пишете код для многопоточной среды, уже достаточно предосторожности, чтобы подчеркнуть, что можно ожидать, что люди не будут знать о состояниях гонки и взаимоблокировках.

Недостатком этого упакованного подхода является то, что каждая операция с типом, который упакован с использованием LockingPtr, должна выполняться через функцию-член. Это повысит уровень косвенности, который может значительно повлиять на комфорт разработчиков в команде.

Но если вы пурист, который верит в дух C ++ a.k.a строгая проверка типов ; это хорошая альтернатива.

4 голосов
/ 22 марта 2010

Это перехватывает некоторые виды небезопасного кода (одновременный доступ), но пропускает другие (взаимоблокировки из-за инверсии блокировки). Ни то, ни другое не особенно легко проверить, так что это скромный частичный выигрыш. На практике, не забывая о применении ограничения на доступ к конкретному частному члену только под определенной блокировкой, для меня не было большой проблемой.

Два ответа на этот вопрос продемонстрировали, что вы правы, когда говорите, что путаница является существенным недостатком - сопровождающие могут быть настолько сильно обусловлены пониманием того, что семантика доступа к памяти volatile не имеет ничего общего с безопасностью потоков, что они даже не прочитает остальную часть кода / статьи, прежде чем объявить ее неверной.

Я думаю, что другим большим недостатком, изложенным Александреску в статье, является то, что он не работает с неклассовыми типами. Это может быть трудным ограничением для запоминания. Если вы считаете, что маркировка элементов данных volatile останавливает их использование без блокировки, а затем вы ожидаете, что компилятор сообщит вам, когда нужно блокировать, тогда вы можете случайно применить это к int или к элементу шаблона-параметра. тип Полученный неверный код скомпилируется нормально, , но вы, возможно, перестали проверять свой код на наличие ошибок такого типа . Представьте себе ошибки, которые могут возникнуть, особенно в шаблонном коде, если бы можно было присвоить const int, но программисты, тем не менее, ожидали, что компилятор проверит их правильность const ...

Я думаю, что риск того, что тип элемента данных действительно имеет какие-либо volatile функции-члены, должен быть отмечен и затем обесценен, хотя это может когда-нибудь кого-нибудь укусить.

Интересно, можно ли что-то сказать о компиляторах, предоставляющих дополнительные модификаторы типа const-стиля через атрибуты? Страуструп говорит: «Рекомендуется использовать атрибуты только для управления вещами, которые не влияют на смысл программы, но могут помочь обнаружить ошибки». Если бы вы могли заменить все упоминания volatile в коде на [[__typemodifier(needslocking)]], то я думаю, что было бы лучше. Тогда было бы невозможно использовать объект без const_cast, и, надеюсь, вы бы не написали const_cast, не подумав о том, что вы отбрасываете.

2 голосов
/ 02 февраля 2011

Опираясь на другой код и полностью исключая необходимость в volatile-спецификаторе, это не только работает, но и правильно распространяет const (аналогично итератору и const_iterator). К сожалению, для двух типов интерфейса требуется довольно много стандартного кода, но вам не нужно повторять какую-либо логику методов: каждый из них по-прежнему определяется один раз, даже если вам необходимо «дублировать» «изменчивые» версии аналогичным образом. нормальной перегрузке методов на const и non-const.

#include <cassert>
#include <iostream>

struct ExampleMutex {  // Purely for the sake of this example.
  ExampleMutex() : _locked (false) {}
  bool try_lock() {
    if (_locked) return false;
    _locked = true;
    return true;
  }
  void lock() {
    bool acquired = try_lock();
    assert(acquired);
  }
  void unlock() {
    assert(_locked);
    _locked = false;
  }
private:
  bool _locked;
};

// Customization point so these don't have to be implemented as nested types:
template<class T>
struct VolatileTraits {
  typedef typename T::VolatileInterface       Interface;
  typedef typename T::VolatileConstInterface  ConstInterface;
};

template<class T>
class Lock;
template<class T>
class ConstLock;

template<class T, class Mutex=ExampleMutex>
struct Volatile {
  typedef typename VolatileTraits<T>::Interface       Interface;
  typedef typename VolatileTraits<T>::ConstInterface  ConstInterface;

  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  Interface       operator*()        { return _data; }
  ConstInterface  operator*() const  { return _data; }
  Interface       operator->()        { return _data; }
  ConstInterface  operator->() const  { return _data; }

private:
  T _data;
  mutable Mutex _mutex;

  friend class Lock<T>;
  friend class ConstLock<T>;
};

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

template<class T>
struct ConstLock {
  ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); }
  ~ConstLock() { _data._mutex.unlock(); }

  T const& operator*() { return _data._data; }
  T const* operator->() { return &**this; }

private:
  Volatile<T> const &_data;
};

struct Something {
  class VolatileConstInterface;
  struct VolatileInterface {
    // A bit of boilerplate:
    VolatileInterface(Something &x) : base (&x) {}
    VolatileInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way");
    }

  private:
    Something *base;

    friend class VolatileConstInterface;
  };

  struct VolatileConstInterface {
    // A bit of boilerplate:
    VolatileConstInterface(Something const &x) : base (&x) {}
    VolatileConstInterface(VolatileInterface x) : base (x.base) {}
    VolatileConstInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way to a const object");
    }

  private:
    Something const *base;
  };

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const {
    std::cout << "do action " << restriction << '\n';
  }
};

int main() {
  Volatile<Something> x;
  Volatile<Something> const c;

  x->action();
  c->action();

  {
    Lock<Something> locked (x);
    locked->action();
  }

  {
    ConstLock<Something> locked (x);  // ConstLock from non-const object
    locked->action();
  }

  {
    ConstLock<Something> locked (c);
    locked->action();
  }

  return 0;
}

Сравните класс Что-то, что потребует от Александреску летучего:

struct Something {
  void action() volatile {
    _do("in a thread-safe way");
  }

  void action() const volatile {
    _do("in a thread-safe way to a const object");
  }

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const volatile {
    std::cout << "do action " << restriction << '\n';
  }
};
2 голосов
/ 02 февраля 2011

C ++ 03 §7.1.5.1p7:

Если предпринята попытка обратиться к объекту, определенному с типом, определенным с помощью volatile, посредством использования lvalue с типом, не определенным с помощью volatile, поведение программы не определено.

Поскольку buffer_ в вашем примере определен как volatile, отбрасывание его является неопределенным поведением. Однако вы можете обойти это с помощью адаптера, который определяет объект как энергонезависимый, но добавляет изменчивость:

template<class T>
struct Lock;

template<class T, class Mutex>
struct Volatile {
  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  T        volatile& operator*()        { return _data; }
  T const  volatile& operator*() const  { return _data; }

  T        volatile* operator->()        { return &**this; }
  T const  volatile* operator->() const  { return &**this; }

private:
  T _data;
  Mutex _mutex;

  friend class Lock<T>;
};

Дружба необходима для строгого контроля энергонезависимого доступа через уже заблокированный объект:

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

Пример:

struct Something {
  void action() volatile;  // Does action in a thread-safe way.
  void action();  // May assume only one thread has access to the object.
  int n;
};
Volatile<Something> data;
void example() {
  data->action();  // Calls volatile action.
  Lock<Something> locked (data);
  locked->action();  // Calls non-volatile action.
}

Есть две оговорки. Во-первых, вы все еще можете получить доступ к открытым элементам данных (Something :: n), но они будут квалифицированы как volatile; это, вероятно, потерпит неудачу в различных точках. И, во-вторых, Нечто не знает, действительно ли оно определено как volatile и отбрасывает, что volatile (из «this» или из членов) в методах все равно будет UB, если оно было определено таким образом:

Something volatile v;
v.action();  // Compiles, but is UB if action casts away volatile internally.

Основная цель достигнута: объекты не должны знать, что они используются таким образом, и компилятор будет предотвращать вызовы энергонезависимых методов (что является большинством методов для большинства типов), если вы явно не пройдете через блокировка.

1 голос
/ 22 марта 2010

Посмотрите на это с другой точки зрения.Когда вы объявляете переменную как const, вы говорите компилятору, что значение не может быть изменено вашим кодом.Но это не значит, что значение не изменится .Например, если вы сделаете это:

const int cv = 123;
int* that = const_cast<int*>(&cv);
*that = 42;

... это вызывает неопределенное поведение в соответствии со стандартом, но на практике что-то случится.Возможно значение будет изменено.Может быть, будет сигфо.Может быть, запустит симулятор полета - кто знает.Дело в том, что вы не знаете, независимо от платформы, что произойдет.Таким образом, кажущееся обещание const не выполняется.Значение может или не может быть на самом деле const.

Теперь, учитывая, что это правда, использует const злоупотребление языком?Конечно, нет.Это все еще инструмент, который предоставляет язык, чтобы помочь вам написать лучший код.Это никогда не будет конечным, основным инструментом для обеспечения того, чтобы значения оставались неизменными - мозг программиста в конечном итоге является этим инструментом - но разве это делает const бесполезным?

Я говорю нет, используяconst как инструмент, помогающий вам писать лучший код, не является нарушением языка.На самом деле, я бы пошел еще дальше и сказал, что это намерение этой функции.

Теперь то же самое относится и к волатильности.Объявление чего-либо как volatile не сделает вашу программу безопасной.Это, вероятно, даже не сделает эту переменную или поток объекта безопасным.Но компилятор будет применять семантику CV-квалификации, и осторожный программист может использовать этот факт, чтобы помочь ему написать лучший код, помогая компилятору определять места, где он может писать ошибку.Точно так же, как компилятор помогает ему, когда он пытается это сделать:

const int cv = 123;
cv = 42;  // ERROR - compiler complains that the programmer is potentially making a mistake

Забудьте об ограждениях памяти и атомарности летучих объектов и переменных, точно так же, как вы давно забыли об истинном постоянстве cv.Но используйте инструменты, которые дает вам язык для написания лучшего кода.Одним из таких инструментов является volatile.

0 голосов
/ 02 февраля 2011

В статье ключевое слово используется скорее как тег required_thread_safety, чем фактическое предполагаемое использование volatile.

Не прочитав статью - почему Андрей тогда не использует упомянутый тег required_thread_safety? Злоупотребление volatile не очень хорошая идея. Я считаю, что это вызывает больше путаницы (как вы сказали), а не избегает этого.

Тем не менее, volatile может иногда требоваться в многопоточном коде, даже если это не достаточное условие, просто чтобы предотвратить компилятор от оптимизации проверок, которые полагаются на асинхронное обновление значения.

0 голосов
/ 22 марта 2010

Я не знаю конкретно, верен ли совет Александреску, но, несмотря на то, что я уважаю его как супер-умного чувака, его отношение к семантике volatile говорит о том, что он вышел за пределы своей области знаний. Volatile не имеет абсолютно никакого значения в многопоточности (см. здесь для хорошего рассмотрения предмета), и поэтому утверждение Александреску о том, что volatile является полезным для многопоточного доступа, заставляет меня всерьез задуматься над тем, сколько веры можете разместить в оставшейся части своей статьи.

0 голосов
/ 22 марта 2010

Тебе лучше не делать этого. volatile даже не был изобретен для обеспечения безопасности потоков. Он был изобретен для правильного доступа к отображенным в памяти аппаратным регистрам. Ключевое слово volatile не влияет на функцию выполнения CPU вне очереди. Вы должны использовать правильные вызовы ОС или определенные CAS инструкции CPU, ограждения памяти и т. Д.

CAS

Ограждение памяти

...