将只读数据安全地传递给新线程
Posted
技术标签:
【中文标题】将只读数据安全地传递给新线程【英文标题】:Safely passing read-only data to a new thread 【发布时间】:2012-03-21 20:45:36 【问题描述】:假设我有一个程序初始化一个供线程使用的全局变量,如下所示:
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。
-
一般性问题:尽管很常见,但这在所有处理器架构中真的安全吗?如果没有,如何确保安全?
全局变量不是
volatile
;在StartThread()
通话后是否可能会重新排序并让我感到厌烦?如何解决这个问题?
假设计算机有两个处理器,它们都有自己的缓存。主线程在第一个处理器上运行,工作线程在第二个处理器上运行。假设在程序开始运行SomeFunction()
之前,包含ThreadParameter
的内存块已经被分页到每个处理器的缓存中。 SomeFunction()
将5
写入ThreadParameter
,它被存储在第一个处理器的缓存中,然后启动在第二个处理器上运行的工作线程。第二个处理器上的WorkerThread()
会不会看到ThreadParameter
的未初始化数据而不是5
的预期值,因为第二个处理器中的内存页面还没有看到来自第一个处理器的更新?
如果需要不同的东西 - 考虑到我可以使用指向更复杂的数据类型的指针而不是简单的 int
,如何最好地处理这个问题,而这些数据类型不一定在多线程环境中使用?李>
如果我的担心没有根据,我不需要担心的具体原因是什么?
【问题讨论】:
SomeFunction 将调用多个 WorkerThreads,我假设。我认为您的问题更像是:“同时阅读会给我带来问题吗?” .... ? 好吧,当使用 POSIX 线程时,您可以使用pthread_once()
解决全局变量初始化难题——我不确定这如何转化为您对“便携”的定义。
我想最好的解决方案是boost:shared_mutex
并将只读复制到每个线程中。
当你需要从 ThreadParameter 中读取时使用锁呢?
我很抱歉,但我需要逐步理解这一点。我一直相信volatile
的存在是为了让程序员可以告诉C(它是C,对吗?)编译器它应该假设其他东西会触及地址。您是否有不想使用volatile
的原因?
【参考方案1】:
根据您的描述,您似乎在启动任何子线程之前正在写入 ThreadParameter(或其他一些数据结构),并且您将永远不会再次写入 ThreadParameter...它存在以根据需要读取,但从未更改初始化后再次;那是对的吗?如果是这样,那么每次子线程想要读取数据时,甚至是第一次读取数据时,都不需要使用任何线程同步系统调用(或处理器/编译器原语)。
volatile 的处理在某种程度上是特定于编译器的;我知道至少对于 PowerPC 的 Diab,有一个关于 volatile 处理的编译器选项:在每次读/写变量后使用 PowerPC EIEIO(或 MBAR)指令,或者不使用它......这个除了禁止与变量相关的编译器优化。 (EIEIO/MBAR 是 PowerPC 禁止处理器本身对 I/O 重新排序的指令;即指令之前的所有 I/O 必须在指令之后的任何 I/O 之前完成)。
从正确性/安全性的角度来看,将其声明为 volatile 并没有什么坏处。但是从实用的角度来看,如果您在 StartThread() 之前足够远地初始化 ThreadParameter,则实际上没有必要将其声明为 volatile(并且不这样做会加快对其的所有后续访问)。几乎任何实质性的函数调用(例如,可能是 printf() 或 cout,或任何系统调用等)都会发出比必要数量级更多的指令,以确保处理器不会在很久以前处理写入调用 StartThread() 之前的 ThreadParameter。实际上,StartThread() 本身几乎肯定会在相关线程实际启动之前执行足够多的指令。所以我建议您实际上不需要将其声明为 volatile,即使您在调用 StartThread() 之前立即对其进行初始化也可能不需要。
现在关于您的问题,如果包含该变量的页面在运行主线程的处理器执行初始化之前已经加载到两个处理器的缓存中会发生什么:如果您使用的是常用的通用平台类似的 CPU,硬件应该已经到位,可以为您处理缓存一致性。在通用平台上遇到缓存一致性问题的地方,无论它们是否是多处理器,是当您的处理器具有单独的指令和数据缓存并且您编写自修改代码时:写入内存的指令与数据无法区分,因此 CPU 不会使指令缓存中的这些位置无效,因此指令缓存中可能存在陈旧指令,除非您随后使指令缓存中的这些位置无效(或者发出您自己的处理器特定的汇编指令,您可能不会允许根据您的操作系统和线程的特权级别进行操作,或者为您的操作系统发出适当的缓存无效系统调用)。但是您所描述的不是自修改代码,因此在这方面您应该是安全的。
您的问题 1 询问如何在所有处理器架构中确保安全。好吧,正如我上面所讨论的,如果您使用的是数据总线已正确桥接的同类处理器,那么您应该是安全的。为多处理器互连而设计的通用处理器具有总线监听协议来检测对共享内存的写入......只要您的线程库正确配置共享内存区域。如果您在嵌入式系统中工作,您可能必须自己在 BSP 中进行配置……对于 PowerPC,您需要查看 MMU/BAT 配置中的 WIMG 位;我不熟悉其他架构,可以为您提供有关这些方面的指导。但是....如果您的系统是自制的,或者您的处理器不是同类,您可能无法指望两个处理器能够窥探彼此的写入;请咨询您的硬件人员以获取建议。
【讨论】:
如果您只编写 ThreadParameter 一次,并且假设您使用的是行为良好的语言和编译器 (C++),请尽可能晚地创建变量,最好在初始化时创建它,并将其设为 const。如果您将在主线程中修改 ThreadParameter 并期望子线程遵守该更改,请尝试在变量上设置等待条件。您将等待主线程在使用它之前编写 ThreadParameter 并且可以在子线程运行时检查新的、挂起的更新。编写良好的过程可以退出自身并使用更新后的值重新运行。【参考方案2】:当你创建一个新线程时,线程的构造与线程函数的开始同步。这意味着您很好 - 您在创建线程之前写入 ThreadParameter,线程在启动后访问它,因此您可以确保写入 发生在之前读取,因此线程可以保证看到正确的值。
(编译器需要确保在线程启动之前完成的所有写入都在新线程中可见。)
【讨论】:
【参考方案3】:-
是的,它是安全的。
不知道。也许:
if( ThreadParameter = 5 ) StartThread();
。不过,一般来说,尽量不要再猜测编译器。
可能不会。如果您在编写代码时不得不担心如此低级的细节,那么控制程序如何在多核机器上执行的逻辑可能无法很好地完成它的工作。
Boost 是您在多线程环境中处理复杂类型的朋友。
【讨论】:
以上是关于将只读数据安全地传递给新线程的主要内容,如果未能解决你的问题,请参考以下文章
编译器说数据不能在线程之间安全地共享,即使数据包装在互斥锁中