安全地将只读数据传递给新线程

时间:2012-03-21 20:45:36

标签: c++ c multithreading thread-safety

假设我有一个初始化全局变量以供线程使用的程序,如下所示:

int ThreadParameter;

// this function runs from the main thread
void SomeFunction() {
    ThreadParameter = 5;

    StartThread(); // some function to start a thread
    // at this point, ThreadParameter is NEVER modified.
}

// this function is run in a background worker thread created by StartThread();
void WorkerThread() {
    PrintValue(ThreadParameter); // we expect this to print "5"
}

这些问题应适用于可能遇到的任何通用处理器架构。我希望解决方案是可移植的 - 不是特定于具有更强内存保证的架构,如x86。

  1. 一般问题:尽管非常普遍,但这在所有处理器架构中是否真的安全?如果不安全怎么做?
  2. 全局变量不是volatile;它是否可能在StartThread()电话后重新排序并让我受到冲击?如何解决这个问题?
  3. 假设计算机有两个具有自己缓存的处理器。主线程在第一个处理器上运行,工作线程在第二个处理器上运行假设在程序开始运行ThreadParameter之前,已将包含SomeFunction()的内存块分页到每个处理器的缓存中。 SomeFunction()5写入ThreadParameter,它存储在第一个处理器的缓存中,然后启动在第二个处理器上运行的工作线程。第二个处理器上的WorkerThread()不会看到ThreadParameter的未初始化数据而不是5的预期值,因为第二个处理器中的内存页面尚未看到来自第一个处理器?
  4. 如果需要不同的东西 - 如果最好处理这个而不是简单的int,我可以使用指向更复杂的数据类型的指针,这些数据类型不一定在多线程环境中使用?
  5. 如果我的担忧没有根据,那么我不需要担心的具体原因是什么?

3 个答案:

答案 0 :(得分:3)

创建新线程时,线程的构造与线程函数的开始同步。这意味着你很好 - 你在创建线程之前写入 ThreadParameter ,并且线程在它们启动后访问它,所以你可以确保写发生在之前读取,所以线程保证看到正确的值。

(编译器需要确保在线程启动之前完成的所有写操作都在新线程中可见。)

答案 1 :(得分:2)

从你的描述中,你似乎在编写ThreadParameter(或其他一些数据结构)之前,在启动任何子线程之前,你永远不会再写入ThreadParameter ......它存在是为了根据需要读取,但从未改变过初始化后再次;那是对的吗?如果是这样,那么每次子线程想要读取数据时,甚至第一次就此问题都没有必要使用任何线程同步系统调用(或处理器/编译器原语)。

volatile的处理有点特定于编译器;我知道至少对于PowerPC的Diab,有一个关于volatile处理的编译器选项:在每次读/写变量后使用PowerPC EIEIO(或MBAR)指令,或者不使用它...另外还禁止与变量关联的编译器优化。 (EIEIO / MBAR是PowerPC关于禁止处理器本身重新排序I / O的指令;即指令之前的所有I / O必须在指令之后的任何I / O之前完成。

从正确/安全的角度来看,宣布它是不稳定的并不会有害。但是从实用的角度来看,如果你在StartThread()之前将ThreadParameter初始化得足够远,那么声明它应该是不稳定的(并且不这样做会加速它的所有后续访问)。几乎任何实质性的函数调用(比如,对于printf()或cout,或者任何系统调用等)都会发出比所需更多的指令,以确保处理器不会在很久以前处理写入调用StartThread()之前的ThreadParameter。实际上,StartThread()本身几乎肯定会在有问题的线程实际启动之前执行足够的指令。所以我建议你不需要声明它是volatile,即使你在调用StartThread()之前立即初始化它也可能不会。

现在关于在运行主线程的处理器执行初始化之前,如果包含该变量的页面已经加载到两个处理器的缓存中会发生什么情况的问题:如果您正在使用通用的通用平台类似的CPU,硬件应该已经到位,以便为您处理缓存一致性。您在通用平台上遇到缓存一致性问题的地方,无论它们是否是多处理器,都是在您的处理器具有单独指令的情况下。数据高速缓存并编写自修改代码:写入内存的指令与数据无法区分,因此CPU不会使指令高速缓存中的那些位置无效,因此指令高速缓存中可能存在陈旧指令,除非您随后使这些位置无效在指令缓存中(发出您自己的处理器特定的汇编指令,根据您的操作系统和线程的权限级别,您可能不允许这样做,或者为您的操作系统发出适当的缓存无效系统调用)。但是你所描述的并不是自我修改的代码,所以在这方面你应该是安全的。

您的问题1询问如何在所有处理器架构中保证安全。好吧,正如我上面所讨论的,如果您使用的数据总线已正确桥接的类似处理器,您应该是安全的。设计用于多处理器互连的通用处理器具有总线侦听协议,用于检测对共享内存的写入...只要您的线程库正确配置共享内存区域即可。如果您在嵌入式系统中工作,则可能需要在BSP中自行配置...对于PowerPC,您需要查看MMU / BAT配置中的WIMG位;我不熟悉其他架构,可以为您提供指导。但是......如果您的系统是自制软件或者您的处理器不是那种类型,那么您可能无法指望两个处理器能够窥探彼此的写入;请咨询您的硬件人员以获取建议。

答案 2 :(得分:1)

  1. 是的,这是安全的。
  2. 不知道。也许:if( ThreadParameter = 5 ) StartThread();。但是,一般来说,尽量不要再猜测编译器。
  3. 可能不是。如果在编写代码时不得不担心这种低级别的细节,那么控制程序在多核机器上执行的逻辑可能无法很好地完成其工作。
  4. Boost是您在多线程环境中处理复杂类型的朋友。