让我们假设有两个线程,A和B。还有一个共享数组:float X[100]
。
线程A一次向数组中依次写入一个元素,每10步更新一个共享变量index
(以安全的方式),该变量指示当前索引,并且还向线程发送信号B.
线程B收到信号后,便以安全的方式读取index
,然后继续读取X
的元素,直到位置index
。
这样做安全吗?线程A真的更新了数组还是只是缓存中的副本?
答案 0 :(得分:2)
这样做安全吗?
只要您的数据修改安全并受到关键部分,锁或其他任何东西的保护,对于涉及硬件访问的访问,这种访问是绝对安全的。
线程A真正更新了数组还是只是缓存中的副本?
只是缓存中的副本。目前,大多数高速缓存都是 write-back ,并且仅在修改后将数据从高速缓存中弹出时才将数据写回到内存中。这极大地提高了内存带宽,尤其是在多核环境中。
所有操作都发生在 就像内存已被更新。
对于共享内存处理器,通常有高速缓存一致性协议(某些实时应用程序的处理器除外)。这些协议的基本思想是每个缓存行都关联一个 state 。
状态描述了有关不同处理器的高速缓存中的行的信息。
例如,这些状态指示该行是仅存在于当前高速缓存中还是由多个高速缓存共享,并且与内存同步,无效……请参见例如流行的MESI高速缓存一致性协议的this description。
那么,写入高速缓存行并且该高速缓存行也存在于另一个处理器中时,会发生什么情况?
由于该状态,高速缓存知道一个或多个其他处理器也具有该行的副本,因此它将发送无效信号。该行将在其他缓存中失效,并且当他们想要读取或写入它时,他们必须重新加载其内容。实际上,这种重新加载将由具有有效副本的缓存来进行,以限制内存访问。
这样,尽管仅将数据写入高速缓存,但其行为类似于将数据写入内存的情况。
尽管硬件在功能上将确保传输的正确性,但必须考虑缓存的存在,以避免性能下降。
假设高速缓存A正在更新一行,而高速缓存B正在读取它。每当高速缓存A写入时,高速缓存B中的行就会失效。而且每当缓存B想要读取它时,如果该行已失效,它必须从缓存A中获取它。这可能导致在缓存之间进行该行的多次传输,并使内存系统效率低下。
因此,对于您的示例,大概10不是一个好主意,您应该使用缓存中的信息来改善发送方和接收方之间的交换。
例如,如果您在具有64个字节的缓存行的pentium上,则应将X声明为
_Alignas(64) float X[100];
这样,X
的起始地址将是64的倍数并且适合高速缓存行边界。 _Alignas
限定词自C17开始存在,并且通过包含stdalign.h,您也可以类似地使用alignas(64)
。在C17之前,大多数编译器都有一些扩展名,以便具有一致的位置。
当然,您应该指示进程B仅在写入完整的64字节行(16个浮点数)时读取数据。
这样,当线程B访问数据时,线程A将不再修改高速缓存行,并且仅在高速缓存A和B之间进行一次初始传输。缓存之间的传输数量的这种减少可能会对性能产生重大影响,具体取决于您的程序。
答案 1 :(得分:2)
一个线程向另一个线程发送信号的每一个明智的方式都可以保证,线程在发送信号之前写入的任何内容都可以保证在线程接收到该信号之后对线程可见。因此,只要您通过提供这种保证的某种方式发送信号,他们几乎都会这样做,那么您就安全了。
请注意,尝试使用没有互斥保护的谓词的条件变量不是一个线程向另一个线程发送信号的明智方式!除其他外,它不能保证您认为收到信号的线程实际上收到了信号。您确实需要确保进行读取的线程实际上收到了进行写入的线程发送的信号。
答案 2 :(得分:1)
如果您正在使用一个变量来跟踪读取索引的准备情况,则该变量受互斥锁保护,并且通过pthread条件变量来完成信号传递,线程B在互斥量下等待,然后是。
如果您使用的是POSIX信号,那么我认为您还需要一个同步机制。在线程A中使用memory_order_release
写入原子变量,然后在线程B中使用memory_order_acquire
读取原子变量,应以最轻便的方式保证写在A之前的原子A应该在写B之后可见它已经读取了原子。
为了获得最佳性能,还应以不使阵列的共享部分越过缓存行边界的方式进行阵列共享(否则,由于错误的共享,性能可能会下降)。