线程中的内存范围共享:确保数据不会卡在缓存中

Posted

技术标签:

【中文标题】线程中的内存范围共享:确保数据不会卡在缓存中【英文标题】:memory range sharing in threads : ensure data is not stuck in cache 【发布时间】:2018-01-24 07:13:59 【问题描述】:

当从一个线程向另一个线程发送内存位置的地址时,如何确保数据没有卡在CPU缓存中,并且第二个线程实际上读取了正确的值? (我正在使用 socketpair() 发送 从一个线程到另一个线程的指针)

还有相关的问题,c++ 编译器如何与线程原语一起找出需要专门处理同步的内存地址。

struct Test  int  fld; 

thread_1 ( ) 
  Test *ptr1 = new Test;
  ptr1->fld = 100;
  ::write(write_fd, &ptr1, sizeof(ptr1));


thread_2 () 
  Test *ptr2;
  ::read(read_fd, &ptr2, sizeof(ptr2));
  // WHAT MAGIC IS REQUIRED TO ENSURE THIS ?
  assert(ptr2->fld == 100 );

【问题讨论】:

【参考方案1】:

如果要在同一进程的线程之间传递值,我会确保std::atomic<int>作为字段的类型,以及相关的setter和getter函数。显然,将指针从一个进程传递到另一个进程根本不起作用,除非它来自保证在两个进程中具有相同地址的内存区域 - 例如共享内存,但是你不应该需要套接字。 ..

编译器通常不知道如何处理缓存,atomic 类型除外(从技术上讲,原子通常使用单独的指令处理,而不是缓存刷新和缓存无效,并且处理器硬件处理相关的“与其他处理器讨论缓存内容”)。

操作系统(当然会受到错误影响)在进程之间或进程内传递时会执行此类操作。但是对于传递指针,你不能依赖它,新接收到的指针值是正确的,但是指针指向的内容不是缓存管理的。

在某些处理器中,您可以使用内存屏障来确保线程之间内存内容的正确顺序。这迫使处理器“在此之前执行所有内存操作”。但是,对于像readwrite 这样的系统调用,操作系统应该为您处理好,并确保在read 开始读取它想要读取的内存之前已正确写入内存存储在套接字缓冲区中,write 在存储您的数据后将有一个内存屏障(在这种情况下是指针的值,但内存屏障会影响该点之前的所有读取和/或写入)。

如果您要实现自己的数据传递原语,并且处理器没有缓存一致性(大多数现代处理器都有),您还需要为写入端添加缓存刷新和缓存对阅读方无效。这取决于架构,在标准 C 或 C++ 中不支持此功能(在某些处理器中,只有 OS 功能 [内核模式] 可以刷新或使缓存内容无效,在其他处理器中,它可以在用户模式代码中完成 -此类操作的粒度也有所不同,可能需要刷新或使整个缓存系统无效,或者可以一次刷新 32、64 或 128 字节的单个行)

【讨论】:

想知道如何将完整的 struct 放入 atomic .. struct TestWrapper std::atomic<Struct> wrapped; ,而 ptr1 和 ptr2 是 *TestWrapper 。 原子类型仅适用于简单类型,因此您必须声明每个字段atomic。或者做点别的……;) 嗯。实际上我也有一个缓冲区,发送到另一个线程写入网络;我想知道这不是一般的设计吗!感谢您的帮助。 如果你有中等大小的数据(以千字节为单位)结构,发送副本可能比发送指针更好[显然不适用于没有序列化机制的类]。请注意,如果您希望非连贯处理器进行缓存同步,则开销很大 - 花费的主要时间是实际将受影响的数据写入内存以及在缓存行大小的块中迭代受影响的内存区域的组合。因此,进行可能适合缓存的副本通常比维护缓存要快。【参考方案2】:

在 C++ 中,您无需关心缓存等实现细节。您唯一需要做的就是确保存在 C++ 发生后关系。

正如 Mats Petersson 的回答所示,std::atomic 是实现这一目标的一种方法。对原子变量的所有访问都是有序的,尽管顺序可能不是静态确定的(即,如果您有两个线程试图写入同一个原子变量,则无法预测哪个写入最后发生)。

另一个强制同步的机制是std::mutex。线程可以尝试锁定互斥体,但一次只能有一个线程锁定互斥体。其他线程将阻塞。编译器将确保当一个线程解锁互斥体并且下一个线程锁定互斥体时,第一个线程的写入可以由第二个线程读取。如果这需要刷新缓存,编译器会安排。

另一个机制是std::atomic_thread_fence。如果您在线程之间共享多个对象(所有对象都在同一方向),这将很有用。您可以使它们中的一个成为原子并“附加”一个栅栏到该原子变量,而不是使它们全部成为原子的。然后,您最后写入原子变量,然后先读取它。显然这最好封装在一个类中。

【讨论】:

以上是关于线程中的内存范围共享:确保数据不会卡在缓存中的主要内容,如果未能解决你的问题,请参考以下文章

GPU存储器架构-- 全局内存 本地内存 寄存器堆 共享内存 常量内存 纹理内存

线程安全问题分析

JUC

Java中线程范围内共享问题

本地缓存过期时间与JVM垃圾回收

mysql 优化