通常,load-acquire / store-release同步是C ++ 11内存模型中最常见的基于内存排序的同步形式之一。它基本上是互斥体提供内存排序的方式。负载获取和存储释放之间的“关键部分”总是在不同的观察者线程之间同步,因为所有观察者线程都会同意获取之后和发布之前发生的事情。
通常,这是通过读取 - 修改 - 写入指令(如比较交换)以及进入关键部分时的获取屏障以及另一个具有释放屏障的读取 - 修改 - 写入指令来实现的。部分。
但在某些情况下,您可能在load-acquire和release-store之间有类似的关键部分 [1],除了只有一个线程实际修改了同步变量。其他线程可能读取同步变量,但只有一个线程实际修改它。在这种情况下,进入临界区时,不需要读 - 修改 - 写指令。您只需要一个简单的商店,因为您没有与试图修改同步标志的其他线程竞争。 (这可能看起来很奇怪,但请注意许多无锁内存回收延迟模式,如用户空间RCU或基于纪元的回收,使用线程本地同步变量,只能由一个线程写入,但由许多线程读取,所以这不是一种奇怪的情况。)
因此,当进入关键部分时,您可以执行以下操作:
sync_var.store(true, ...);
.... critical section ....
sync_var.store(false, std::memory_order_release);
没有竞争,因为当只有一个线程需要设置/取消设置临界区变量时,不需要进行读 - 修改 - 写。其他线程可以通过load-acquire简单地读取临界区变量。
问题是,当您进入关键部分时,您需要获取操作或围栏。但是你不需要做一个LOAD,只需要一个STORE。那么,当您真正需要商店时,制作获取订购的好方法是什么?我看到只有两个真正的选项属于C ++内存模型。之一:
exchange
代替商店,因此您可以执行sync_var.exchange(true, std::memory_order_acquire)
。这里的缺点是,当你真正需要的只是一个简单的商店时,交换是一个更重量级的读 - 修改 - 写操作。插入“虚拟”加载获取,如:
(无效)sync_var.load(标准:: memory_order_acquire); sync_var.store(true,std :: memory_order_relaxed);
“虚拟”负载获取似乎更好。据推测,编译器无法优化掉未使用的负载,因为它是一个原子指令,具有与sync_var
上的释放操作产生“同步 - ”关系的副作用。但它似乎也非常hacky,如果没有评论解释正在发生的事情,目的还不清楚。
那么当我们需要做的只是一个简单的商店时,生成获取语义的最佳方法是什么?
[1]我松散地使用术语“临界区”。我不一定是指通过互斥方式访问的部分。相反,我只是指通过acquire-release语义同步内存排序的任何部分。这可能是指一个互斥体,或者它可能只是指像RCU,其中关键部分可以被多个读者同时访问。
答案 0 :(得分:3)
您的逻辑中的缺陷是不需要原子RMW,因为关键部分中的数据由单个线程修改,而所有其他线程仅具有读访问权。 这不是真的;在阅读和写作之间仍然需要有明确的顺序。 您不希望在另一个线程仍在读取数据时修改数据。因此,每个线程在完成访问数据时都需要通知其他线程。
只使用原子商店进入关键部分,'同步 - '关系无法建立。
获取/释放同步基于运行时关系,其中收集器仅在观察到原子负载返回的特定值之后才知道同步完成。
由于一个修改线程可以随时更改原子变量sync_var
,因此单个原子库永远无法实现
因此,它无法知道另一个线程是否仍在读取数据。
使用'虚拟' load/acquire
也无效,因为它无法通知其他线程它想要独占访问。您试图通过使用单个(放松)商店来解决这个问题,
但是加载和存储是可以被其他线程中断的单独操作(即多个线程同时访问关键区域)。
每个线程必须使用原子RMW加载特定值,同时更新变量以通知其现在具有独占访问权限的所有其他线程 (无论是阅读还是写作)。
void lock()
{
while (sync_var.exchange(true, std::memory_order_acquire));
}
void unlock()
{
sync_var.store(false, std::memory_order_release);
}
在多个线程同时具有读访问权限的情况下(例如std::shared_mutex
),可以进行优化。