我正在将一个在裸机上运行的项目迁移到linux,需要消除一些{disable,enable}_scheduler
调用。:)
因此,我需要一个在单个编写器、多个读取器场景中的无锁同步解决方案,其中编写器线程不能被阻塞。我想出了以下解决方案,它不适合通常的获取发布顺序:
class RWSync {
std::atomic<int> version; // incremented after every modification
std::atomic_bool invalid; // true during write
public:
RWSync() : version(0), invalid(0) {}
template<typename F> void sync(F lambda) {
int currentVersion;
do {
do { // wait until the object is valid
currentVersion = version.load(std::memory_order_acquire);
} while (invalid.load(std::memory_order_acquire));
lambda();
std::atomic_thread_fence(std::memory_order_seq_cst);
// check if something changed
} while (version.load(std::memory_order_acquire) != currentVersion
|| invalid.load(std::memory_order_acquire));
}
void beginWrite() {
invalid.store(true, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
}
void endWrite() {
std::atomic_thread_fence(std::memory_order_seq_cst);
version.fetch_add(1, std::memory_order_release);
invalid.store(false, std::memory_order_release);
}
}
我希望其意图是明确的:我将(非原子)有效载荷的修改封装在beginWrite/endWrite
之间,并仅在传递给sync()
的lambda函数内部读取有效载荷。
正如您所看到的,这里我在beginWrite()
中有一个原子存储,其中存储操作之后的任何写入都不能在存储之前重新排序。我没有找到合适的例子,而且我在这个领域根本没有经验,所以我想确认一下它是可以的(通过测试进行验证也不容易)。
这段代码是免费的吗?
如果我在每个原子操作中都使用std::memory_order_seq_cst,我可以省略栅栏吗?(即使是,我想性能会更差)
我可以在endWrite()中放下围栏吗?
我可以在围栏里使用memory_order_aq_rel吗?我真的不明白区别——我不清楚单一总订单的概念。
是否存在简化/优化机会?
+1.我很乐意接受任何更好的想法作为这个类的名称:)
代码基本正确。
您可以使用语义为"奇数值无效"的单个version
变量,而不是使用两个原子变量(version
和invalid
)。这就是所谓的"顺序锁定"机制。
减少原子变量的数量可以简化很多事情:
class RWSync {
// Incremented before and after every modification.
// Odd values mean that object in invalid state.
std::atomic<int> version;
public:
RWSync() : version(0) {}
template<typename F> void sync(F lambda) {
int currentVersion;
do {
currentVersion = version.load(std::memory_order_seq_cst);
// This may reduce calls to lambda(), nothing more
if(currentVersion | 1) continue;
lambda();
// Repeat until something changed or object is in an invalid state.
} while ((currentVersion | 1) ||
version.load(std::memory_order_seq_cst) != currentVersion));
}
void beginWrite() {
// Writer may read version with relaxed memory order
currentVersion = version.load(std::memory_order_relaxed);
// Invalidation requires sequential order
version.store(currentVersion + 1, std::memory_order_seq_cst);
}
void endWrite() {
// Writer may read version with relaxed memory order
currentVersion = version.load(std::memory_order_relaxed);
// Release order is sufficient for mark an object as valid
version.store(currentVersion + 1, std::memory_order_release);
}
};
注意beginWrite()
和endWrite()
:中内存顺序的差异
endWrite()
确保以前对象的所有修改都已完成。使用版本内存顺序就足够了。beginWrite()
确保在开始任何后续对象的修改之前,读取器将检测到对象处于无效状态。这样的garantee需要seq_cst内存顺序。因为读卡器也使用seq_cst内存顺序。
至于fence,最好将它们合并到上一个/下一个原子操作中:编译器知道如何快速生成结果。
对原始代码的一些修改的解释:
1)原子修改如fetch_add()
适用于可能进行并发修改(如另一个fetch_add()
)的情况。为了正确起见,此类修改使用内存锁定或其他非常耗时的体系结构特定的东西。
原子分配(store()
)不使用内存锁定,因此它比fetch_add()
便宜。您可以使用这样的分配,因为在您的情况下不可能同时修改(读者不会修改version
)。
2) 与区分load
和store
操作的发布获取语义不同,顺序一致性(memory_order_seq_cst
)适用于每个原子访问,并提供这些访问之间的总顺序。
接受的答案不正确。我想代码应该类似于"currentVersion&1",而不是"currentVersion|1"。更微妙的错误是,读线程可以进入lambda(),然后写线程可以运行beginWrite()并将值写入非原子变量。在这种情况下,负载中的写操作和负载中的读操作在关系之前并没有发生。对非原子变量的并发访问(没有先发生后发生的关系)是一场数据竞赛。注意,memory_order_seq_cst的单个总顺序并不意味着先发生后发生的关系;它们是一致的,但有两种。