我知道,在之前的几个问题/答案中已经非常清楚地表明,易失性与 C++ 内存模型的可见状态有关,而不是与多线程有关。
另一方面,Alexandrescu 的这篇文章使用了 volatile 关键字不是作为运行时功能,而是作为编译时检查,以强制编译器无法接受可能不是线程安全的代码。在本文中,关键字的使用更像是 required_thread_safety
标记,而不是 volatile
的实际预期用途。
这种(ab)使用易失性
合适吗?该方法中可能隐藏着哪些可能的陷阱?
我首先想到的是增加混乱:易失性
与线程安全无关,但由于缺乏更好的工具,我可以接受它。
文章的基本简化:
如果声明一个变量易失性
,则只能在其上调用易失性
成员方法,因此编译器将阻止调用其他方法的代码。将 std::vector
实例声明为 易失性
将阻止该类的所有使用。添加一个锁定指针形式的包装器,该包装器执行 const_cast 来释放 易失性 要求,通过锁定指针进行的任何访问都将被允许。
摘自文章:
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_
};
注意
在前几个答案出现后,我想我必须澄清,因为我可能没有使用最合适的词语。
使用易失性
并不是因为它在运行时提供什么,而是因为它在编译时意味着什么。也就是说,如果 const 关键字在用户定义类型中很少使用,那么可以使用 const 关键字来实现相同的技巧,就像 volatile 一样。也就是说,有一个关键字(碰巧拼写为 volatile)允许我阻止成员函数调用,Alexandrescu 正在使用它来欺骗编译器,使其无法编译线程不安全代码。
我将其视为许多元编程技巧,这些技巧的存在不是因为它们在编译时所做的事情,而是因为它强制编译器为您做的事情。
I know, it has been made quite clear in a couple of questions/answers before, that volatile
is related to the visible state of the c++ memory model and not to multithreading.
On the other hand, this article by Alexandrescu uses the volatile
keyword not as a runtime feature but rather as a compile time check to force the compiler into failing to accept code that could be not thread safe. In the article the keyword is used more like a required_thread_safety
tag than the actual intended use of volatile
.
Is this (ab)use of volatile
appropriate? What possible gotchas may be hidden in the approach?
The first thing that comes to mind is added confusion: volatile
is not related to thread safety, but by lack of a better tool I could accept it.
Basic simplification of the article:
If you declare a variable volatile
, only volatile
member methods can be called on it, so the compiler will block calling code to other methods. Declaring an std::vector
instance as volatile
will block all uses of the class. Adding a wrapper in the shape of a locking pointer that performs a const_cast
to release the volatile
requirement, any access through the locking pointer will be allowed.
Stealing from the article:
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_
};
NOTE
After the first couple of answers have appeared I think I must clarify, as I might not have used the most appropriate words.
The use of volatile
is not because of what it provides at runtime but because of what it means at compile time. That is, the same trick could be pulled with the const
keyword if it was as rarely used in user defined types is as volatile
is. That is, there is a keyword (that happens to be spelled volatile) that allows me to block member function calls, and Alexandrescu is using it to trick the compiler into failing to compile thread-unsafe code.
I see it as many metaprogramming tricks that are there not because of what they do at compile time, but rather for what it forces the compiler to do for you.
发布评论
评论(8)
我认为问题不在于 volatile 提供的线程安全性。事实并非如此,Andrei 的文章也没有这么说。这里,使用
互斥体
来实现这一点。问题是,使用volatile
关键字提供静态类型检查以及对线程安全代码使用互斥体是否属于滥用易失性
关键字?恕我直言,这非常聪明,但我遇到过一些开发人员,他们只是为了严格类型检查而不喜欢严格类型检查。IMO,当您为多线程环境编写代码时,已经有足够的谨慎来强调您希望人们不要忽视竞争条件和死锁。
这种包装方法的缺点是,使用 LockingPtr 包装的类型上的每个操作都必须通过成员函数进行。这将增加一个间接级别,这可能会极大地影响开发人员在团队中的舒适度。
但如果您是一个相信 C++ 精神(又名严格类型检查)的纯粹主义者;这是一个很好的选择。
I think the issue is not about thread-safety provided by
volatile
. It dosen't and Andrei's article dosen't say it does. Here, amutex
is used to achieve that. The issue is, whether the use ofvolatile
keyword to provide static type-checking along with use of mutex for thread-safe code, is abuse of thevolatile
keyword? IMHO it's pretty smart, but i have come across developers who are not fans of strict-type-checking just for the sake of it.IMO when you are writing code for multi-threaded environment, there is already enough caution to emphasize wherein you would expect people not to be ignorant of race-conditions and deadlocks.
A downside of this wrapped approach is that every operation on the type that is wrapped using
LockingPtr
must be through a member function. That will increase one level of indirection which might considerably affect developers comfort in a team.But if you are a purist who believes in the spirit of C++ a.k.a strict-type-checking; this is a good alternative.
这捕获了某些类型的线程不安全代码(并发访问),但错过了其他类型(由于锁定反转而导致的死锁)。两者都不是特别容易测试,所以这是一个适度的部分胜利。在实践中,记住强制执行一个约束,即只能在某些指定的锁定下访问特定的私有成员,这对我来说并不是一个大问题。
这个问题的两个答案已经证明你说的混乱是一个显着的缺点是正确的 - 维护者可能已经强烈地理解 易失性的内存访问语义与线程安全无关,他们甚至不会在声明它不正确之前,请阅读代码/文章的其余部分。
我认为 Alexandrescu 在文章中概述的另一个大缺点是它不适用于非类类型。这可能是一个很难记住的限制。如果您认为标记数据成员
易失性
会阻止您在不锁定的情况下使用它们,然后期望编译器告诉您何时锁定,那么您可能会意外地将其应用于int
,或模板参数依赖类型的成员。生成的错误代码可以正常编译,但是您可能已经停止检查代码是否存在此类错误。想象一下,如果可以分配给 const int ,会发生什么错误,尤其是在模板代码中,但程序员仍然希望编译器会检查它们的 const 正确性......我认为风险应该注意数据成员的类型实际上具有任何
易失性
成员函数,然后忽略这一点,尽管有一天它可能会咬伤某人。我想知道编译器通过属性提供额外的 const 样式类型修饰符是否有什么可说的。 Stroustrup 说,“建议使用属性仅控制不影响程序含义但可能有助于检测错误的事物”。如果您可以用
[[__typemodifier(needslocking)]]
替换代码中所有提到的volatile
,那么我认为会更好。如果没有const_cast
就不可能使用该对象,并且希望您在编写const_cast
时不会考虑要丢弃的内容。This catches some kinds of thread-unsafe code (concurrent access), but misses others (deadlocks due to locking inversion). Neither is especially easy to test for, so it's a modest partial win. In practice, remembering to enforce a constraint that a particular private member is accessed only under some specified lock, hasn't been a big problem for me.
Two answers to this question have demonstrated that you're correct to say that confusion is a significant disadvantage - maintainers may have been so strongly conditioned to understand that volatile's memory-access semantics have nothing to do with thread-safety, that they will not even read the rest of the code/article before declaring it incorrect.
I think the other big disadvantage, outlined by Alexandrescu in the article, is that it doesn't work with non-class types. This might be a difficult restriction to remember. If you think that marking your data members
volatile
stops you using them without locking, and then expect the compiler to tell you when to lock, then you might accidentally apply that to anint
, or to a member of template-parameter-dependent type. The resulting incorrect code will compile fine, but you may have stopped examining your code for errors of this kind. Imagine the errors which would occur, especially in template code, if it was possible to assign to aconst int
, but programmers nevertheless expected the compiler would check const-correctness for them...I think the risk that the data member's type actually has any
volatile
member functions should be noted and then discounted, although it might bite somebody someday.I wonder if there's anything to be said for compilers providing additional const-style type modifiers via attributes. Stroustrup says, "The recommendation is to use attributes to only control things that do not affect the meaning of a program but might help detect errors". If you could replace all mentions of
volatile
in the code with[[__typemodifier(needslocking)]]
then I think it would be better. It would then be impossible to use the object without aconst_cast
, and hopefully you wouldn't write aconst_cast
without thinking about what it is you're discarding.C++03 §7.1.5.1p7:
因为您的示例中的 buffer_ 被定义为易失性,所以将其丢弃是未定义的行为。但是,您可以使用适配器来解决这个问题,该适配器将对象定义为非易失性,但增加了波动性:
需要友谊来严格控制通过已锁定对象的非易失性访问:
示例:
有两个注意事项。首先,您仍然可以访问公共数据成员(Something::n),但它们将被限定为 volatile;这可能会在不同的地方失败。其次,Something 不知道它是否真的被定义为 易失性的,并且在方法中抛弃该易失性(来自“this”或来自成员),如果它是这样定义的,那么它仍然是 UB:
主要目标已实现:对象不必知道它们以这种方式使用,并且编译器将阻止调用非易失性方法(这是大多数类型的所有方法),除非您显式地使用锁。
C++03 §7.1.5.1p7:
Because buffer_ in your example is defined as volatile, casting it away is undefined behavior. However, you can get around that with an adapter which defines the object as non-volatile, but adds volatility:
The friendship is needed to strictly control non-volatile access through an already locked object:
Example:
There are two caveats. First, you can still access public data members (Something::n), but they will be qualified volatile; this will probably fail at various points. And second, Something doesn't know if it really has been defined as volatile and casting away that volatile (from "this" or from members) in methods will still be UB if it has been defined that way:
The main goal is achieved: objects don't have to be aware that they are used this way, and the compiler will prevent calls to non-volatile methods (which is all methods for most types) unless you explicitly go through a lock.
构建于其他代码并完全消除对 易失性说明符的需要,这不仅可以工作,而且可以正确传播 const (类似于 iterator 与 const_iterator)。不幸的是,这两种接口类型需要相当多的样板代码,但您不必重复任何方法逻辑:每个接口仍然定义一次,即使您必须类似地“复制”“易失性”版本const 和非 const 方法的正常重载。
将 Something 类与 Alexandrescu 使用 volatile 所需的内容进行比较:
Building on other code and removing the need for the volatile specifier entirely, this not only works, but correctly propagates const (similar to iterator vs const_iterator). Unfortunately, it requires quite a bit of boilerplate code for the two interface types, but you don't have to repeat any logic of methods: each is still defined once, even if you do have to "duplicate" the "volatile" versions similarly to normal overloading of methods on const and non-const.
Compare class Something to what Alexandrescu's use of volatile would require:
从不同的角度来看这个问题。当您将变量声明为 const 时,您就告诉编译器该值不能被您的代码更改。但这并不意味着该值不会改变。例如,如果您这样做:
...这会根据标准引发未定义的行为,但实际上会发生一些事情。也许值会改变。也许会出现 sigfault。也许飞行模拟器会推出——谁知道呢。关键是你不知道在独立于平台的基础上会发生什么。因此,const 的表面承诺并未实现。该值实际上可能是也可能不是 const。
现在,既然这是真的,那么使用
const
是否是对语言的滥用?当然不是。它仍然是该语言提供的一个工具,可以帮助您编写更好的代码。它永远不会是确保值保持不变的最终工具——程序员的大脑最终就是那个工具——但这是否会使 const 变得无用?我说不,使用 const 作为工具来帮助你编写更好的代码并不是对语言的滥用。事实上,我会更进一步,说这是该功能的意图。
现在,易失性也是如此。将某些内容声明为 易失性不会使您的程序线程安全。它甚至可能不会使该变量或对象线程安全。但编译器将强制执行 CV 限定语义,细心的程序员可以利用这一事实,帮助编译器识别可能编写错误的位置,从而帮助他编写更好的代码。就像编译器在他尝试执行此操作时帮助他一样:
忘记内存栅栏以及易失性对象和变量的原子性,就像您早已忘记
cv
的真正常量一样。但使用该语言提供的工具可以编写更好的代码。这些工具之一是易失性
。Look at this from a different perspective. When you declare a variable as const, you are telling the compiler that the value cannot be changed by your code. But that doesn't mean that the value won't change. For example, if you do this:
...this evokes undefined behavior according to the standard, but in practice something will happen. Maybe the value will be changed. Maybe there will be a sigfault. Maybe flight simulator will launch -- who knows. The point is you don't know on a platform-independant basis what's going to happen. So the apparent promise of
const
is not fulfilled. The value may or may not actually be const.Now, given that this is true, is using
const
an abuse of the language? Of course not. It is still a tool that the language provides to help you write better code. It will never be the end-all, be-all tool to ensure that values remain unchanged -- the programmer's brain is ultimately that tool -- but does that makeconst
unuseful?I say no, using const as a tool to help you write better code is not an abuse of the language. In fact I'd go one step further, and say it is the intent of that feature.
Now, the same is true of volatile. Declaring something as volatile will not make your program thread safe. It probably won't even make that variable or object thread safe. But the compiler will enforce CV-qualification semantics, and careful programmer can leverage this fact to help him write better code by helping the compiler to identify places where he might be writing a bug. Just like the compiler helps him when he tries to do this:
Forget about memory fences and atomicity of volatile objects and variables, just like you have long forgotten about
cv
's true constness. But use the tools that the language gives you to write better code. One of those tools isvolatile
.你最好不要这样做。 易失性甚至不是为了提供线程安全而发明的。它的发明是为了正确访问内存映射的硬件寄存器。 易失性关键字对CPU的乱序执行功能没有影响。您应该使用正确的操作系统调用或 CPU 定义的 CAS 指令、内存栅栏等。
CAS
内存栅栏
You must better not do that. volatile was not even invented to provide thread-safety. It was invented to access memory-mapped hardware registers properly. volatile keyword has no effect over CPU's out-of-order execution feature. You should use proper OS calls or CPU defined CAS instructions, memory fences, etc.
CAS
Memory Fence
在没有阅读这篇文章的情况下 – 为什么 Andrei 不使用所说的
required_thread_safety
标签呢?滥用易失性
在这里听起来并不是一个好主意。我相信这会导致更多混乱(就像你说的),而不是避免它。也就是说,有时在多线程代码中可能需要
易失性
,即使这不是一个充分条件,只是为了防止编译器优化依赖于异步更新的检查一个值。Without having read the article – why isn’t Andrei using said
required_thread_safety
tag then? Abusingvolatile
doesn’t sound such a good idea here. I believe this causes more confusion (like you said), rather than avoiding it.That said,
volatile
may sometimes be required in multi-threaded code even if it’s not a sufficient condition, just to prevent the compiler from optimizing away checks that rely on asynchronous update of a value.我具体不知道 Alexandrescu 的建议是否合理,但是,尽管我尊重他作为一个超级聪明的家伙,但他对 volatile 语义的处理表明他已经远远超出了他的专业领域。 Volatile 在多线程中绝对没有价值(请参阅 here 是为了更好地处理这个主题),因此 Alexandrescu 声称 volatile 对于多线程访问很有用,这让我认真地想知道我能有多少信心在他文章的其余部分。
I don't know specifically whether Alexandrescu's advice is sound, but, for all that I respect him as a super-smart dude, his treatment of volatile's semantics suggests that he's stepped way outside his area of expertise. Volatile has absolutely no value in multithreading (see here for a good treatment of the subject) and so Alexandrescu's claim that volatile is useful for multithreaded access leads me to seriously wonder how much faith I can place in the rest of his article.