易失性和创建线程
我刚刚问了一个涉及易失性的问题:易失性数组c++
但是我的问题引发了关于什么的讨论易失性
确实如此。
有人声称,使用CreateThread()
时,您不必担心易失性
。 另一方面,微软给出了一个使用CreateThread()
创建的两个线程时易失性
的例子。
我在 Visual C++ Express 2010 中创建了以下示例,无论您是否将 done
标记为 易失性
都没有关系,
#include "targetver.h"
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <tchar.h>
using namespace std;
bool done = false;
DWORD WINAPI thread1(LPVOID args)
{
while(!done)
{
}
cout << "Thread 1 done!\n";
return 0;
}
DWORD WINAPI thread2(LPVOID args)
{
Sleep(1000);
done = 1;
cout << "Thread 2 done!\n";
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD thread1Id;
HANDLE hThread1;
DWORD thread2Id;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, &thread1Id);
hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, &thread2Id);
Sleep(4000);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
您能否始终确保线程 1 在以下情况下停止: done
不是 volatile
吗?
I just asked a question involving volatile: volatile array c++
However my question spawned a discussion on what volatile
does.
Some claim that when using the CreateThread()
, you don't have to worry about volatiles
.
Microsoft on the other hand gives an example of volatile
when using two threads created by CreateThread()
.
I created the following sample in visual c++ express 2010, and it doesn't matter if you mark done
as volatile
or not
#include "targetver.h"
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <tchar.h>
using namespace std;
bool done = false;
DWORD WINAPI thread1(LPVOID args)
{
while(!done)
{
}
cout << "Thread 1 done!\n";
return 0;
}
DWORD WINAPI thread2(LPVOID args)
{
Sleep(1000);
done = 1;
cout << "Thread 2 done!\n";
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD thread1Id;
HANDLE hThread1;
DWORD thread2Id;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, &thread1Id);
hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, &thread2Id);
Sleep(4000);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
Can you ALWAYS be sure that thread 1 will stop if done
is not volatile
?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
volatile
的作用:易失性
不做什么:在跨平台 C++ 中不应依赖的一些不可移植行为:
易失性
以防止与其他指令进行任何重新排序。其他编译器不这样做,因为它会对优化产生负面影响。大多数时候,人们真正想要的是栅栏(也称为屏障)和原子指令,如果您有 C++11 编译器,或者通过依赖于编译器和体系结构的编译器,则可以使用它们否则起作用。
栅栏确保在使用时,所有先前的读/写都将完成。在 C++11 中,使用 std::memory_order 枚举在各个点控制栅栏。在 VC++ 中,您可以使用
_ReadBarrier()
、_WriteBarrier()
和_ReadWriteBarrier()
来执行此操作。我不确定其他编译器的情况。在某些架构(例如 x86)上,栅栏只是防止编译器对指令重新排序的一种方法。在其他情况下,它们实际上可能会发出一条指令来阻止 CPU 本身对事物进行重新排序。
下面是一个不正确使用的示例:
这里,
finished
允许重新排序到res
设置之前!好吧,易失性可以防止与其他易失性重新排序,对吗?让我们尝试使每个res
也变得易失:这个简单的示例实际上可以在 x86 上运行,但效率会很低。首先,这会强制
res1
在res2
之前设置,即使我们并不真正关心这一点......我们只是希望它们都在之前设置完成了。在
res1
和res2
之间强制执行此排序只会阻止有效的优化,从而降低性能。对于更复杂的问题,您必须让每个都写入
易失性
。这会使你的代码变得臃肿,非常容易出错,并且变得很慢,因为它阻止了比你真正想要的更多的重新排序。这不现实。所以我们使用栅栏和原子。它们允许完全优化,并且仅保证内存访问将在栅栏点完成:
这适用于所有体系结构。
res1
和res2
操作可以按照编译器认为合适的方式重新排序。执行原子释放可确保所有非原子操作按顺序完成,并且对执行原子获取的线程可见。What
volatile
does:What
volatile
does not:Some non-portable behaviors that shouldn't be relied on in cross-platform C++:
volatile
to prevent any reordering with other instructions. Other compilers don't, because it negatively affects optimization.Most of the time, what people really want are fences (also called barriers) and atomic instructions, which are usable if you've got a C++11 compiler, or via compiler- and architecture-dependent functions otherwise.
Fences ensure that, at the point of use, all the previous reads/writes will be completed. In C++11, fences are controlled at various points using the
std::memory_order
enumeration. In VC++ you can use_ReadBarrier()
,_WriteBarrier()
, and_ReadWriteBarrier()
to do this. I'm not sure about other compilers.On some architectures like x86, a fence is merely a way to prevent the compiler from reordering instructions. On others they might actually emit an instruction to prevent the CPU itself from reordering things.
Here's an example of improper use:
Here,
finished
is allowed to be reordered to before eitherres
is set! Well, volatile prevents reordering with other volatile, right? Let's try making eachres
volatile too:This trivial example will actually work on x86, but it is going to be inefficient. For one, this forces
res1
to be set beforeres2
, even though we don't really care about that... we just want both of them set beforefinished
is. Forcing this ordering betweenres1
andres2
will only prevent valid optimizations, eating away at performance.For more complex problems, you'll have to make every write
volatile
. This would bloat your code, be very error prone, and become slow as it prevents a lot more reordering than you really wanted.It's not realistic. So we use fences and atomics. They allow full optimization, and only guarantee that the memory access will complete at the point of the fence:
This will work for all architectures.
res1
andres2
operations can be reordered as the compiler sees fit. Performing an atomic release ensures that all non-atomic ops are ordered to complete and be visible to threads which perform an atomic acquire.易失性
只是阻止编译器对声明的易失性
值进行假设(读取:优化)访问。换句话说,如果你声明了一些易失性
,你基本上是在说它可能会因为编译器不知道的原因随时改变它的值,所以每当你引用这个变量时,它都必须查找当时的价值。在这种情况下,编译器可能决定将
done
的值实际缓存在处理器寄存器中,而与其他地方可能发生的更改无关 - 即线程 2 将其设置为true
。我猜想它在您的示例中起作用的原因是对
done
的所有引用实际上都是done
在内存中的真实位置。您不能期望情况总是如此,尤其是当您开始请求更高级别的优化时。此外,我想指出,使用
volatile
关键字进行同步并不合适。它可能碰巧是原子的,但只是根据情况而定。我建议您使用实际的线程同步构造,例如等待条件或互斥体。请参阅 http ://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/ 提供了精彩的解释。volatile
simply prevents the compiler from making assumptions (read:optimizing) access to the value declaredvolatile
. In other words, if you declare somethingvolatile
, you are basically saying it may change it's value at any time for reasons the compiler is not aware of, so any time you reference the variable it must look up the value at that time.In this instance, the compiler might decide to actually cache
done
's value in a processor register, independent of changes that might happen elsewhere - i.e. thread 2 setting it totrue
.I would guess the reason it worked in your example is all references to
done
were actually the real location ofdone
in memory. You cannot expect this to always be the case, especially when you start requesting higher levels of optimization.Additionally, I would like to point out that it is a not an appropriate use of the
volatile
keyword for synchronization. It might happen to be atomic, but only by circumstance. I would advise you to use an actualy thread synchronization construct like await condition
ormutex
instead. See http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/ for a fantastic explanation.总是?不会。但在这种情况下,对
done
的赋值位于同一模块中,并且while
循环可能不会被优化。取决于 MSVC 如何执行优化。一般来说,使用 volatile 声明它会更安全,以避免优化带来的不确定性。
Always? No. But in this case the assignment to
done
is in the same module, and thewhile
loop will probably not be optimized out. Depends on how the MSVC performs its optimizations.Generally, it is safer to declare it with
volatile
to avoid uncertainty with optimizations.实际上,这比您想象的更糟糕 - 某些编译器可能会认为该循环是无操作或 无限循环,消除无限循环的情况,并使其立即返回无论做什么。而且编译器肯定可以自由地将
done
保存在本地CPU寄存器中,并且永远不会在循环中访问其更新的值。您必须使用适当的内存屏障,或易失性标志变量(这在技术上在某些 CPU 架构上是不够的),或像这样的标志的锁保护变量。It's worse than you think, actually - some compilers may decide that that loop is either a no-op or infinite loop, eliminate the infinite loop case, and make it return immediately no matter what done is. And the compiler is most certainly free to keep
done
in a local CPU register and never access its updated value in the loop. You must either use appropriate memory barriers, or a volatile flag variable (this technically isn't enough on certain CPU architectures), or a lock-protected variable for a flag like this.在linux上编译,g++ 4.1.2,我输入了与您的示例等效的内容:
当使用-O3编译时,编译器缓存了该值,因此它检查一次,如果第一次没有完成,则进入无限循环。
但是,然后我将程序更改为以下内容:
虽然这仍然是一个旋转(它只是重复锁定/解锁互斥锁),但编译器更改了调用以始终在从 pthread_mutex_unlock 返回后检查 did 的值,从而使其正常工作适当地。
进一步的测试表明,调用任何外部函数似乎都会导致它重新检查变量。
Compiling on linux, g++ 4.1.2, I put in the equivalent of your example:
When compiled with -O3, the compiler cached the value, so it checked once and then enters an infinite loop if it wasn't done the first time.
However, then I changed the program to the following:
While this is still a spin (it just repeatedly locks/unlocks the mutex), the compiler changed the call to always check the value of done after the return from pthread_mutex_unlock, causing it to work properly.
Further tests show that calling any external function appears to cause it to re-examine the variable.
易失性
不是同步机制。它不保证原子性或顺序。如果您不能保证对共享资源执行的所有操作都是原子的,那么您必须使用适当的锁定!最后,我强烈建议阅读这些文章:
volatile
IS NOT a synchronisation mechanism. It DOES NOT guarantee atomicity nor ordering. If you cannot guarantee that all operations performed on a shared resource are atomic, then you MUST use proper locking!Finally, I highly recommend reading these articles: