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

Safely passing read-only data to a new thread

本文关键字:新线程 线程 只读 数据安全      更新时间:2023-10-16

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

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. 假设计算机有两个处理器,它们有自己的缓存。主线程运行在第一个处理器上,工作线程运行在第二个处理器上。假设在程序开始运行SomeFunction()之前,包含ThreadParameter的内存块已被分页到每个处理器的缓存中。SomeFunction()5写入存储在第一个处理器的缓存中的ThreadParameter,然后启动在第二个处理器上运行的工作线程。第二个处理器上的WorkerThread()不会看到ThreadParameter的未初始化数据,而不是5的预期值吗
  4. 如果需要一些不同的东西——如果不是简单的int,我可以使用一个指向更复杂的数据类型的指针,而这些数据类型不一定在多线程环境中使用,那么如何最好地处理它

如果我的担忧是没有根据的,那么我不需要担心的具体原因是什么?

根据您的描述,在启动任何子线程之前,您似乎正在写入ThreadParameter(或其他数据结构),并且您将永远不会再写入ThreadParameter。。。它的存在是为了根据需要进行读取,但在初始化后再也不会更改;这是正确的吗?如果是这样,那么在每次子线程想要读取数据时,甚至在第一次读取数据时都不需要使用任何线程同步系统调用(或处理器/编译器原语)。

volatile的处理在某种程度上是编译器特有的;我知道,至少在Diab for PowerPC中,有一个关于volatile处理的编译器选项:要么在每次读取/写入变量后使用PowerPC EIEIO(或MBAR)指令,要么不使用它……这是禁止与变量相关的编译器优化的补充。(EIEIO/MBAR是PowerPC的指令,用于禁止处理器本身对I/O进行重新排序;即,指令之前的所有I/O都必须在指令之后的任何I/O之前完成)。

从正确性/安全性的角度来看,将其声明为易失性并没有坏处。但从实用的角度来看,如果您在StartThread()之前足够早地初始化ThreadParameter,那么声明它volatile就没有必要了(不这样做会加快对它的所有后续访问)。几乎任何实质性的函数调用(比如,可能对printf()或cout,或任何系统调用等)都会发出数量级以上的指令,以确保处理器在调用StartThread()之前不会很久就处理对ThreadParameter的写入。实际上,StartThread()本身几乎肯定会在有问题的线程真正启动之前执行足够的指令。因此,我建议您不需要真正声明它volatile,即使在调用StartThread()之前立即初始化它,也可能不需要。

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

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

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

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

  1. 是的,它是安全的
  2. 不知道。也许:if( ThreadParameter = 5 ) StartThread();。但是,一般情况下,尽量不要对编译器进行事后猜测
  3. 可能不会。如果你在编写代码时不得不担心如此低级别的细节,那么控制程序如何在多核机器上执行的逻辑可能做得不太好
  4. Boost是您在多线程环境中处理复杂类型的朋友