有时可以在C++中使用std::atomic代替std::mutex吗?

我想std::atomic有时可以取代std::mutex. 但是使用 atomic 而不是 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那么系统不可用,我不能使用它,如果是真的,那么它可用,我可以使用它。为简单起见,假设上面的代码不抛出异常。

因为我可以std::mutex用来保护我的系统的读取/修改。但是现在我需要在 Thread-1 中使用非常高性能的代码,这些代码应该经常使用原子而不是互斥锁(线程 2 可能很慢,如果需要,可以使用互斥锁)。

在 Thread-1 系统使用代码(在 while 循环内)经常运行,每次迭代都围绕50-200 nano-seconds. 所以使用额外的互斥锁会很繁重。但是 Thread-2 迭代非常大,正如您可以在 while 循环的每次迭代中看到的那样,当系统准备好时,它会休眠 for 10-30 milli-seconds,因此仅在 Thread-2 中使用互斥锁是可以的。

Thread-1 是一个线程的示例,在我的实际项目中,有多个线程运行与 Thread-1 相同(或非常相似)的代码。

我担心内存操作顺序,这意味着当系统ready变为trueThread-1时,它可能会发生在系统尚未处于完全一致状态(尚未完全初始化)的情况下。也有可能发生readyfalse在线程1为时已晚,当系统中已经取得了一定的破坏(DEINIT)操作。同样如您所见,系统可以在 Thread-2 的循环中多次初始化/销毁,并在 Thread-1 中多次使用ready

如果没有 Thread-1 中的 std::mutex 和其他繁重的东西,我的任务可以以某种方式解决吗?仅使用 std::atomic(或 std::atomic_flag)。如果需要,线程 2 可以使用大量同步的东西,互斥体等。

基本上线程2应该以某种方式传播系统的整体inited状态到所有内核和其它线程之前ready变得true和也是线程2将传播ready等于false系统破坏(DEINIT)的任何单一的小操作之前就完成了。通过传播状态我的意思是所有系统的inited数据应该是100%一致写入全局内存等核心的缓存,让其他线程看到完全一致的系统,只要readytrue

如果可以改善情况和保证,甚至允许在系统初始化之后和 ready 设置为 true 之前进行小(毫秒)暂停。并且还允许在 ready 设置为 false 之后和开始系统销毁(deinit)之前暂停。如果存在诸如“将所有 Thread-2 写入传播到全局内存并将缓存传播到所有其他 CPU 内核和线程”之类的操作,那么在 Thread-2 中执行一些昂贵的 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

上面的这个解决方案9 nanoseconds在我的 Windows 10 64 位 2Ghz 2 核笔记本电脑上的单线程(发布编译)中平均运行(测量 2^25 个操作)。虽然std::unique_lock<std::mutex> lock(mux);用于相同的保护目的需要100-120 nanoseconds在同一台 Windows PC 上。如果线程需要自旋锁而不是在等待时休眠,那么std::this_thread::yield();我只使用分号而不是上面的代码;。使用和时间测量的完整在线示例。

回答

为了答案,我将忽略您的代码,答案通常是肯定的。

锁有以下作用:

  1. 在任何给定时间只允许一个线程获取它
  2. 当获得锁时,会放置一个读屏障
  3. 在释放锁之前,放置了一个写屏障

以上 3 点的组合使临界区线程安全。只有一个线程可以访问共享内存,由于读屏障,所有更改都由锁定线程观察,并且由于写屏障,所有更改都对其他锁定线程可见。

你能用原子来实现吗?是的,现实生活中的锁(例如,由 Win32/Posix 提供)是通过使用原子和无锁编程实现的,或者通过使用使用原子的锁和无锁编程来实现。

现在,实际上,您应该使用自写锁而不是标准锁吗?绝对不。

许多并发教程保留了自旋锁比常规锁“更有效”的概念。我怎么强调它是多么愚蠢。用户模式自旋锁永远不会比操作系统提供的锁更有效。原因很简单,操作系统锁连接到操作系统调度程序。因此,如果一个锁试图锁定一个锁并且失败了 - 操作系统知道冻结这个线程并且不会重新安排它运行直到锁被释放。

使用用户模式自旋锁,这不会发生。操作系统无法知道相关线程试图在紧密循环中获取锁。Yielding 只是一个补丁而不是解决方案——我们想旋转一小段时间,然后进入睡眠状态直到锁被释放。使用用户模式自旋锁,我们可能会浪费整个线程量子试图锁定自旋锁并让步。

老实说,我会说,最近的 C++ 标准确实让我们能够在原子上休眠,等待它改变它的值。所以我们可以以一种非常蹩脚的方式实现我们自己的“真正的”锁,尝试旋转一段时间然后休眠直到锁被释放。但是,当您不是并发专家时,实现正确有效的锁几乎是不可能的。

我自己的哲学观点是,在 2021 年,开发人员应该很少处理那些非常低级的并发主题。将这些事情留给内核人员。使用一些高级并发库并专注于您想要开发的产品,而不是微优化您的代码。这是并发性,其中正确性 >>> 效率。

Linus Torvalds 的相关咆哮


以上是有时可以在C++中使用std::atomic代替std::mutex吗?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>