易失性和创建线程

发布于 2024-11-26 21:31:58 字数 1215 浏览 1 评论 0原文

我刚刚问了一个涉及易失性的问题:易失性数组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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

太傻旳人生 2024-12-03 21:31:58

volatile 的作用:

  • 防止编译器优化任何访问。每次读/写都会产生一条读/写指令。
  • 防止编译器对与其他易失性变量的访问重新排序

易失性 不做什么:

  • 使访问成为原子的。
  • 防止编译器使用非易失性访问重新排序。
  • 使一个线程中的更改在另一个线程中可见。

在跨平台 C++ 中不应依赖的一些不可移植行为:

  • VC++ 扩展了 易失性 以防止与其他指令进行任何重新排序。其他编译器不这样做,因为它会对优化产生负面影响。
  • x86 使指针大小和较小变量的对齐读/写成为原子的,并且立即对其他线程可见。其他架构则不然。

大多数时候,人们真正想要的是栅栏(也称为屏障)和原子指令,如果您有 C++11 编译器,或者通过依赖于编译器和体系结构的编译器,则可以使用它们否则起作用。

栅栏确保在使用时,所有先前的读/写都将完成。在 C++11 中,使用 std::memory_order 枚举在各个点控制栅栏。在 VC++ 中,您可以使用 _ReadBarrier()_WriteBarrier()_ReadWriteBarrier() 来执行此操作。我不确定其他编译器的情况。

在某些架构(例如 x86)上,栅栏只是防止编译器对指令重新排序的一种方法。在其他情况下,它们实际上可能会发出一条指令来阻止 CPU 本身对事物进行重新排序。

下面是一个不正确使用的示例:

int res1, res2;
volatile bool finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished = true;
}

void spinning_thread()
{
    while(!finished); // spin wait for res to be set.
}

这里,finished 允许重新排序到 res 设置之前!好吧,易失性可以防止与其他易失性重新排序,对吗?让我们尝试使每个 res 也变得易失:

volatile int res1, res2;
volatile bool finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished = true;
}

void spinning_thread()
{
    while(!finished); // spin wait for res to be set.
}

这个简单的示例实际上可以在 x86 上运行,但效率会很低。首先,这会强制 res1res2 之前设置,即使我们并不真正关心这一点......我们只是希望它们都在 之前设置完成了。在 res1res2 之间强制执行此排序只会阻止有效的优化,从而降低性能。

对于更复杂的问题,您必须让每个都写入易失性。这会使你的代码变得臃肿,非常容易出错,并且变得很慢,因为它阻止了比你真正想要的更多的重新排序。

这不现实。所以我们使用栅栏和原子。它们允许完全优化,并且仅保证内存访问将在栅栏点完成:

int res1, res2;
std::atomic<bool> finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished.store(true, std::memory_order_release);
}

void spinning_thread()
{
    while(!finished.load(std::memory_order_acquire));
}

这适用于所有体系结构。 res1res2 操作可以按照编译器认为合适的方式重新排序。执行原子释放可确保所有非原子操作按顺序完成,并且对执行原子获取的线程可见。

What volatile does:

  • Prevents the compiler from optimizing out any access. Every read/write will result in a read/write instruction.
  • Prevents the compiler from reordering the access with other volatiles.

What volatile does not:

  • Make the access atomic.
  • Prevent the compiler from reordering with non-volatile accesses.
  • Make changes from one thread visible in another thread.

Some non-portable behaviors that shouldn't be relied on in cross-platform C++:

  • VC++ has extended volatile to prevent any reordering with other instructions. Other compilers don't, because it negatively affects optimization.
  • x86 makes aligned read/write of pointer-sized and smaller variables atomic, and immediately visible to other threads. Other architectures don't.

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:

int res1, res2;
volatile bool finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished = true;
}

void spinning_thread()
{
    while(!finished); // spin wait for res to be set.
}

Here, finished is allowed to be reordered to before either res is set! Well, volatile prevents reordering with other volatile, right? Let's try making each res volatile too:

volatile int res1, res2;
volatile bool finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished = true;
}

void spinning_thread()
{
    while(!finished); // spin wait for res to be set.
}

This trivial example will actually work on x86, but it is going to be inefficient. For one, this forces res1 to be set before res2, even though we don't really care about that... we just want both of them set before finished is. Forcing this ordering between res1 and res2 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:

int res1, res2;
std::atomic<bool> finished;

void work_thread(int a, int b)
{
    res1 = a + b;
    res2 = a - b;
    finished.store(true, std::memory_order_release);
}

void spinning_thread()
{
    while(!finished.load(std::memory_order_acquire));
}

This will work for all architectures. res1 and res2 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.

篱下浅笙歌 2024-12-03 21:31:58

易失性只是阻止编译器对声明的易失性值进行假设(读取:优化)访问。换句话说,如果你声明了一些易失性,你基本上是在说它可能会因为编译器不知道的原因随时改变它的值,所以每当你引用这个变量时,它都必须查找当时的价值。

在这种情况下,编译器可能决定将 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 declared volatile. In other words, if you declare something volatile, 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 to true.

I would guess the reason it worked in your example is all references to done were actually the real location of done 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 a wait condition or mutex instead. See http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/ for a fantastic explanation.

流年已逝 2024-12-03 21:31:58

你能否始终确保线程 1 在完成后不会停止
不稳定

总是?不会。但在这种情况下,对 done 的赋值位于同一模块中,并且 while 循环可能不会被优化。取决于 MSVC 如何执行优化。

一般来说,使用 volatile 声明它会更安全,以避免优化带来的不确定性。

Can you ALWAYS be sure that thread 1 will stop if done is not
volatile?

Always? No. But in this case the assignment to done is in the same module, and the while 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.

七分※倦醒 2024-12-03 21:31:58

实际上,这比您想象的更糟糕 - 某些编译器可能会认为该循环是无操作或 无限循环,消除无限循环的情况,并使其立即返回无论做什么。而且编译器肯定可以自由地将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.

疯到世界奔溃 2024-12-03 21:31:58

在linux上编译,g++ 4.1.2,我输入了与您的示例等效的内容:

#include <pthread.h>

bool done = false;

void* thread_func(void*r) {
  while(!done) {};
  return NULL;
}

void* write_thread_func(void*r) {
  done = true;
  return NULL;
}


int main() {
  pthread_t t1,t2;
  pthread_create(&t1, NULL, thread_func, NULL);
  pthread_create(&t2, NULL, write_thread_func, NULL);
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
}

当使用-O3编译时,编译器缓存了该值,因此它检查一次,如果第一次没有完成,则进入无限循环。

但是,然后我将程序更改为以下内容:

#include <pthread.h>

bool done = false;
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;

void* thread_func(void*r) {
  pthread_mutex_lock(&mu);
  while(!done) {
    pthread_mutex_unlock(&mu);
    pthread_mutex_lock(&mu);
  };
  pthread_mutex_unlock(&mu);
  return NULL;
}

void* write_thread_func(void*r) {

  pthread_mutex_lock(&mu);
  done = true;
  pthread_mutex_unlock(&mu);
  return NULL;
}


int main() {
  pthread_t t1,t2;
  pthread_create(&t1, NULL, thread_func, NULL);
  pthread_create(&t2, NULL, write_thread_func, NULL);
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
}

虽然这仍然是一个旋转(它只是重复锁定/解锁互斥锁),但编译器更改了调用以始终在从 pthread_mutex_unlock 返回后检查 did 的值,从而使其正常工作适当地。

进一步的测试表明,调用任何外部函数似乎都会导致它重新检查变量。

Compiling on linux, g++ 4.1.2, I put in the equivalent of your example:

#include <pthread.h>

bool done = false;

void* thread_func(void*r) {
  while(!done) {};
  return NULL;
}

void* write_thread_func(void*r) {
  done = true;
  return NULL;
}


int main() {
  pthread_t t1,t2;
  pthread_create(&t1, NULL, thread_func, NULL);
  pthread_create(&t2, NULL, write_thread_func, NULL);
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
}

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:

#include <pthread.h>

bool done = false;
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;

void* thread_func(void*r) {
  pthread_mutex_lock(&mu);
  while(!done) {
    pthread_mutex_unlock(&mu);
    pthread_mutex_lock(&mu);
  };
  pthread_mutex_unlock(&mu);
  return NULL;
}

void* write_thread_func(void*r) {

  pthread_mutex_lock(&mu);
  done = true;
  pthread_mutex_unlock(&mu);
  return NULL;
}


int main() {
  pthread_t t1,t2;
  pthread_create(&t1, NULL, thread_func, NULL);
  pthread_create(&t2, NULL, write_thread_func, NULL);
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
}

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.

撩发小公举 2024-12-03 21:31:58

易失性 不是同步机制。它保证原子性或顺序。如果您不能保证对共享资源执行的所有操作都是原子的,那么您必须使用适当的锁定

最后,我强烈建议阅读这些文章:

  1. 易失性:对于多线程编程几乎没用
  2. 是否应该获取原子性和线程可见性语义?

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:

  1. Volatile: Almost Useless for Multi-Threaded Programming
  2. Should volatile Acquire Atomicity and Thread Visibility Semantics?
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文