我知道,之前在几个问题/答案中已经清楚地表明,volatile
与c ++内存模型的可见状态有关,而与多线程有关。
另一方面,Alexandrescu的这个article不使用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正在使用它来欺骗编译器无法编译线程不安全的代码。
我认为很多元编程技巧不是因为它们在编译时所做的,而是因为它迫使编译器为你做的事情。
答案 0 :(得分:6)
我认为问题与volatile
提供的线程安全无关。它不会和安德烈的文章没有说它确实如此。在这里,使用mutex
来实现这一点。问题是,使用volatile
关键字提供 静态类型检查 以及使用互斥锁进行线程安全代码是否滥用{ {1}}关键字?恕我直言,它非常聪明,但我遇到的开发人员不仅仅是为了它而不是严格类型检查的粉丝。
IMO在为多线程环境编写代码时,已经有足够的谨慎强调,你会期望人们不要对种族条件和死锁一无所知。
这种包装方法的缺点是使用volatile
包装的类型上的每个操作都必须通过成员函数。这将增加一个间接水平,这可能会极大地影响开发人员在团队中的舒适度。
但如果你是一个相信C ++精神的纯粹主义者,那么 严格类型检查 ;这是一个很好的选择。
答案 1 :(得分:4)
这会捕获某些线程不安全的代码(并发访问),但会错过其他代码(由于锁定反转而导致的死锁)。两者都不是特别容易测试,所以这是一个适度的部分胜利。在实践中,记住强制执行仅在某些指定锁定下访问特定私有成员的约束对我来说不是一个大问题。
这个问题的两个答案已经证明你说错误是一个显着的缺点是正确的 - 维护者可能已经非常强烈地理解volatile的内存访问语义与线程安全无关,他们在宣布错误之前,我们甚至不会阅读其余的代码/文章。
我认为Alexandrescu在文章中概述的另一个重大缺点是它不适用于非类型。这可能是一个难以记住的限制。如果您认为标记数据成员volatile
会阻止您使用它们而不锁定,然后期望编译器告诉您何时锁定,那么您可能会意外地将其应用于int
或成员模板参数依赖类型。生成的错误代码将正常编译,但您可能已停止检查代码是否存在此类错误。想象一下可能发生的错误,特别是在模板代码中,如果可以分配给const int
,但程序员仍然希望编译器检查它们的const正确性......
我认为应该注意数据成员的类型实际上具有任何volatile
成员函数的风险,然后打折,尽管有一天它可能会咬人。
我想知道编译器是否可以通过属性提供额外的const-style类型修饰符。 Stroustrup says,“建议使用属性来仅控制不影响程序含义但可能有助于检测错误的内容”。如果您可以使用volatile
替换代码中[[__typemodifier(needslocking)]]
的所有提及,那么我认为会更好。没有const_cast
就不可能使用该对象,并且希望你不会在不考虑丢弃它的情况下编写const_cast
。
答案 2 :(得分: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.
实现了主要目标:对象不必知道它们是以这种方式使用的,并且编译器将阻止对非易失性方法的调用(这是大多数类型的所有方法),除非您明确地通过锁。
答案 3 :(得分:2)
Building on other code并且完全不需要volatile说明符,这不仅有效,而且正确传播const(类似于iterator vs const_iterator)。不幸的是,它需要两个接口类型的相当多的样板代码,但是您不必重复任何方法逻辑:每个仍然定义一次,即使您必须同样“复制”“volatile”版本在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';
}
};
答案 4 :(得分:1)
从不同的角度来看这个。当您将变量声明为const时,您告诉编译器您的代码无法更改该值。但这并不意味着值不会更改。例如,如果您这样做:
const int cv = 123;
int* that = const_cast<int*>(&cv);
*that = 42;
...这会根据标准唤起未定义的行为,但在实践中会发生一些事情。也许价值会改变。也许会有一个sigfault。也许飞行模拟器将启动 - 谁知道。关键是你不知道平台独立的基础会发生什么。因此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
。
答案 5 :(得分:0)
你最好不要那样做。甚至没有发明 volatile 来提供线程安全性。它的发明是为了正确访问内存映射的硬件寄存器。 volatile 关键字对CPU的乱序执行功能没有影响。您应该使用正确的OS调用或CPU定义的CAS指令,内存栅栏等
答案 6 :(得分:0)
在文章中,关键字的使用方式更像
required_thread_safety
标签,而非挥发性的实际用途。
没有阅读文章 - 为什么Andrei不使用required_thread_safety
标签呢?在这里滥用volatile
并不是一个好主意。我相信这会导致更多混淆(就像你说的那样),而不是避免它。
也就是说,多线程代码中有时可能需要volatile
,即使它不是足够的条件,只是为了防止编译器优化掉依赖于异步更新的检查。价值。
答案 7 :(得分:-2)
我不清楚Alexandrescu的建议是否合理,但是,尽管我尊重他作为一个超级聪明的家伙,但他对易变性语义的处理表明他已经超越了他的专业领域。 Volatile在多线程中绝对没有价值(请参阅here以获得对该主题的良好处理),因此Alexandrescu声称volatile 对于多线程访问非常有用,这让我非常想知道我有多少信心放在他的文章的其余部分。