我认为std::atomic
有时可以替代std::mutex
的使用。但是,使用原子操作是否总是安全的呢?以下是示例代码:
std::atomic_flag f, ready; // shared
// ..... Thread 1 (and others) ....
while (true) {
// ... Do some stuff in the beginning ...
while (f.test_and_set()); // spin, acquire system lock
if (ready.test()) {
UseSystem(); // .... use our system for 50-200 nanoseconds ....
}
f.clear(); // release lock
// ... Do some stuff at the end ...
}
// ...... Thread 2 .....
while (true) {
// ... Do some stuff in the beginning ...
InitSystem();
ready.test_and_set(); // signify system ready
// .... sleep for 10-30 milli-seconds ....
while (f.test_and_set()); // acquire system lock
ready.clear(); // signify system shutdown
f.clear(); // release lock
DeInitSystem(); // finalize/destroy system
// ... Do some stuff at the end ...
}
这里我使用
std::atomic_flag
来保护我的系统(一些复杂的库)的使用。但这段代码是否安全?这里假设如果ready
为false
,那么系统不可用,我无法使用它;如果为true
,则可用,我可以使用它。为简单起见,假设上面的代码不会抛出异常。当然,我可以使用
std::mutex
来保护系统的读取/修改操作。但现在我需要在Thread-1中使用高性能代码,需要经常使用原子操作而不是互斥量(如果需要,Thread-2可以慢慢使用互斥量)。在Thread-1中,系统使用代码(while循环内部)非常频繁地运行,每次迭代大约需要花费
50-200纳秒
。因此,使用额外的互斥量会很重。但是Thread-2的迭代非常大,如您在while循环的每次迭代中所看到的,当系统就绪时,它会休眠10-30毫秒
,因此仅在Thread-2中使用互斥量是可以接受的。Thread-1是一个线程的示例,在我的实际项目中有几个线程运行相同(或非常相似)的代码。
我担心的是内存操作顺序,这意味着当
ready
在Thread-1中变为true
时,系统可能仍未完全处于一致状态(尚未完全初始化)。此外,当Thread-1中ready
变为false
太晚时,系统已经执行了某些销毁(去初始化)操作。此外,正如您所看到的,在Thread-2的循环中,系统可以被初始化/销毁多次,并在它是ready
时被多次使用。我的任务是否可以在没有std::mutex和Thread-1中的其他繁重内容的情况下解决?只使用std::atomic(或std::atomic_flag)。如果需要,Thread-2可以使用重量级同步工具,如互斥量等。
基本上,Thread-2应该在
ready
变为true
之前以某种方式将整个系统初始化状态传播到所有核心和其他线程,并且还应在进行任何单个小操作的系统销毁(去初始化)之前将ready
等于false
的状态传播出去。通过传播状态,我指的是所有系统的已初始化数据都应该100%一致地写入全局内存和其他核心的缓存中,以便其他线程在ready
为true
时看到完全一致的系统。如果有助于改善情况并保证,甚至可以在系统初始化后和ready设置为true之前暂停一小段时间(毫秒级)。在ready设置为false之后开始系统销毁(去初始化)之前也可以暂停。如果存在"将所有Thread-2写操作传播到全局内存和所有其他CPU核心和线程"等操作,则进行一些昂贵的CPU操作也是可以接受的。
更新: 针对我上面的问题,在我的项目中,我决定使用以下代码来解决,使用std::atomic_flag
替换std::mutex
:
std::atomic_flag f = ATOMIC_FLAG_INIT; // shared
// .... Later in all threads ....
while (f.test_and_set(std::memory_order_acquire)) // try acquiring
std::this_thread::yield();
shared_value += 5; // Any code, it is lock-protected.
f.clear(std::memory_order_release); // release
以上解决方案在我的Windows 10 64位2Ghz双核笔记本电脑上,使用单线程(发布编译)平均运行9纳秒
(测量2^25次操作)。而在同一台Windows PC上,为了实现相同的保护目的,使用std::unique_lock<std::mutex> lock(mux);
需要100-120纳秒
的时间。如果需要线程在等待时自旋而不是休眠,则可以在上述代码中使用分号;
代替std::this_thread::yield();
。完整的在线示例,包括时间测量。
std::unique_lock<std::mutex> lock(mux);
这一行代码的执行时间大约为100-120纳秒
。而在Linux上,它要快得多,大约只需要20-30纳秒
。而atomic<size_t>/atomic_flag在Windows和Linux上都只需要17纳秒
。这些测试都是在CLang release-O3
下进行的。对我来说,17纳秒比100-120纳秒更可取。 - Arty