Может быть изменен в пользовательских типах, чтобы помочь писать потокобезопасный код

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

С другой стороны, эта статья Alexandrescu использует ключевое слово 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. То есть, есть ключевое слово (которое, случается, записывается волатильно), что позволяет мне блокировать вызовы функций-членов, а Alexandrescu использует его, чтобы обмануть компилятор, чтобы не скомпилировать небезопасный код.

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

Ответ 1

Я думаю, что проблема не в обеспечении безопасности потоков, предоставляемых volatile. Это не так, и статья Андрея не говорит, что это так. Для этого используется mutex. Проблема заключается в том, что использование ключевого слова volatile для обеспечения статического контроля типов наряду с использованием мьютекса для потокобезопасного кода является злоупотреблением ключевым словом volatile? IMHO это довольно умный, но я столкнулся с разработчиками, которые не являются поклонниками строгого контроля только ради этого.

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

С++ 03 §7.1.5.1p7:

Если делается попытка обратиться к объекту, определенному с помощью типа с изменчивой квалификацией, с использованием значения lval с типом, не зависящим от летучих, поведение программы undefined.

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

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 (из "this" или из членов) в методах все равно будет UB, если он был определен таким образом:

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

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

Ответ 4

Основываясь на другом коде и полностью устраняя необходимость в спецификаторе volatile, это не только работает, но и корректно распространяется на const (аналогично итератору vs const_iterator). К сожалению, для двух типов интерфейсов требуется довольно много кода шаблона, но вам не нужно повторять какую-либо логику методов: каждый из них по-прежнему определяется один раз, даже если вам придется "дублировать" "изменчивые" версии аналогично к нормальной перегрузке методов на const и не 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;
}

Сравнить класс Кое-что, к чему потребует Alexandrescu volatile:

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';
  }
};

Ответ 5

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

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

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

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

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

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

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

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

Ответ 6

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

CAS

Запоминание памяти

Ответ 7

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

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

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

Ответ 8

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