繁体   English   中英

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

[英]Can std::atomic be used sometimes instead of std::mutex in C++?

我想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来保护我的系统(一些复杂的库)的使用。 但它是安全的代码吗? 在这里,我假设如果readyfalse则系统不可用并且我无法使用它,如果它为 true 则它可用并且我可以使用它。 为简单起见,假设上面的代码不会引发异常。

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

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

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

我担心true操作顺序,这意味着当在 Thread-1 中ready时,系统可能还没有完全一致的 state(尚未完全启动)。 此外,当系统已经进行了一些破坏( false )操作时,可能会在 Thread-1 中为时已晚ready 此外,正如您所见,系统可以在 Thread-2 的循环中多次启动/销毁,并在 Thread-1 ready时多次使用。

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

基本上,线程 2 应该以某种方式将系统的整个初始化 state 在ready变为true之前传播到所有内核和其他线程,并且线程 2 应该在系统破坏(deinit)的任何单个小操作完成之前传播ready等于false 通过传播 state 我的意思是所有系统的初始化数据应该 100% 一致地写入全局 memory 和其他内核的缓存,以便其他线程在readytrue时看到完全一致的系统。

如果它可以改善情况和保证,甚至可以在系统初始化之后和 ready 设置为 true 之前进行小(毫秒)暂停。 并且还允许在 ready 设置为 false 之后和开始系统破坏 (deinit) 之前进行暂停。 如果存在一些操作,例如“将所有 Thread-2 写入传播到全局 memory 并将缓存传播到所有其他 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

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

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

锁做以下事情:

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

以上 3 点的结合使临界区线程安全。 只有一个线程可以接触到共享的 memory,由于读屏障,所有更改都被锁定线程观察到,并且由于写屏障,所有更改都对其他锁定线程可见。

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

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

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

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

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

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

Linus Torvalds 的相关咆哮

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM