25

我知道,之前在几个问题/答案中已经很清楚了,这volatile与 c++ 内存模型的可见状态有关,而不是与多线程有关。

另一方面,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。也就是说,有一个关键字(恰好拼写为 volatile)允许我阻止成员函数调用,而 Alexandrescu 正在使用它来欺骗编译器,使其无法编译线程不安全的代码。

我认为它存在许多元编程技巧,这些技巧不是因为它们在编译时做了什么,而是因为它迫使编译器为你做些什么。

4

8 回答 8

6

我认为问题不在于volatile. 它没有,安德烈的文章也没有说它有。在这里, amutex用于实现这一点。问题是,使用volatile关键字提供静态类型检查以及使用互斥锁来提供线程安全代码是否是volatile关键字的滥用?恕我直言,这很聪明,但我遇到过不喜欢严格类型检查的开发人员。

IMO 当您为多线程环境编写代码时,已经足够谨慎地强调您希望人们不要对竞争条件和死锁一无所知。

这种包装方法的一个缺点是,对使用包装的类型的每个操作都LockingPtr必须通过成员函数。这将增加一层间接性,这可能会极大地影响开发人员在团队中的舒适度。

但是如果你是一个相信 C++ 精神的纯粹主义者,也就是严格类型检查;这是一个很好的选择。

于 2010-03-22T11:40:13.843 回答
4

这会捕获某些类型的线程不安全代码(并发访问),但会错过其他代码(由于锁定反转导致的死锁)。两者都不是特别容易测试,所以这是一个适度的部分胜利。在实践中,记住强制一个特定的私有成员只能在某个指定的锁下访问,这对我来说并不是一个大问题。

这个问题的两个答案已经证明你说混淆是一个明显的缺点是正确的 - 维护者可能已经非常习惯于理解 volatile 的内存访问语义与线程安全无关,他们甚至不会在声明它不正确之前阅读其余的代码/文章。

我认为 Alexandrescu 在文章中概述的另一个大缺点是它不适用于非类类型。这可能是一个难以记住的限制。如果您认为标记您的数据成员会volatile阻止您在不锁定的情况下使用它们,然后期望编译器告诉您何时锁定,那么您可能会不小心将其应用于int,或模板参数相关类型的成员。生成的错误代码可以正常编译,但您可能已经停止检查代码是否存在此类错误。想象一下会发生的错误,特别是在模板代码中,如果可以分配给 a const int,但程序员仍然希望编译器会检查它们的 const 正确性......

我认为volatile应该注意数据成员的类型实际上具有任何成员函数的风险,然后打折,尽管它可能有一天会咬人。

我想知道对于通过属性提供额外的 const 样式类型修饰符的编译器是否有什么要说的。Stroustrup 说,“建议使用属性只控制不影响程序含义但可能有助于检测错误的事物”。volatile如果您可以将代码中的所有提及替换为[[__typemodifier(needslocking)]]then 我认为会更好。没有 a 就不可能使用该对象const_cast,并且希望您不会在const_cast不考虑您要丢弃的内容的情况下编写 a 。

于 2010-03-22T13:35:38.180 回答
2

基于其他代码构建并完全消除对 volatile 说明符的需求,这不仅有效,而且正确传播 const(类似于 iterator vs 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;
}

将 Something 类与 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';
  }
};
于 2011-02-02T16:44:20.093 回答
2

C++03 §7.1.5.1p7:

如果尝试通过使用具有非 volatile 限定类型的左值来引用使用 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;这可能会在各个方面失败。其次,Something 不知道它是否真的被定义为 volatile 并且在方法中丢弃该 volatile(来自“this”或来自成员)仍然是 UB,如果它是这样定义的:

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

主要目标已实现:对象不必知道它们是以这种方式使用的,并且编译器将阻止调用非易失性方法(这是大多数类型的所有方法),除非您显式通过锁。

于 2011-02-02T14:10:18.993 回答
1

从不同的角度来看这个。当您将变量声明为 const 时,您是在告诉编译器该值不能被您的代码更改。但这并不意味着价值不会改变。例如,如果您这样做:

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

...根据标准,这会引起未定义的行为,但实际上会发生一些事情。也许值会改变。也许会有一个sigfault。也许飞行模拟器会启动——谁知道呢。关键是你不知道在独立于平台的基础上会发生什么。所以表面上的承诺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.

于 2010-03-22T14:27:03.007 回答
0

在本文中,关键字的使用更像是一个required_thread_safety标签,而不是 volatile 的实际预期用途。

没有读过这篇文章——那么安德烈为什么不使用上述required_thread_safety标签呢?在这里滥用volatile听起来不是一个好主意。我相信这会导致更多的混乱(就像你说的那样),而不是避免它。

也就是说,volatile有时在多线程代码中可能需要它,即使它不是充分条件,只是为了防止编译器优化掉依赖于值的异步更新的检查。

于 2011-02-02T17:55:31.027 回答
0

你最好不要那样做。volatile甚至不是为了提供线程安全而发明的。发明它是为了正确访问内存映射的硬件寄存器。volatile关键字对 CPU 的乱序执行特性没有影响。您应该使用正确的操作系统调用或 CPU 定义的 CAS 指令、内存栅栏等。

中国科学院

记忆栅栏

于 2010-03-22T11:18:58.637 回答
-2

我不知道 Alexandrescu 的建议是否正确,但尽管我尊重他是一个超级聪明的家伙,但他对 volatile 语义的处理表明他已经超出了他的专业领域。Volatile 在多线程中绝对没有价值(请参阅此处以获得对该主题的良好处理),因此 Alexandrescu 声称 volatile对多线程访问很有用,这让我严重怀疑我对他文章的其余部分有多大的信心。

于 2010-03-22T11:19:22.443 回答