【问题标题】:C++17 atomics and condition_variable deadlockC++17 原子和条件变量死锁
【发布时间】:2018-04-03 05:17:47
【问题描述】:

我有以下代码,它在注释行上死锁。基本上 f1 和 f2 在程序中作为单独的线程运行。 f1 期望 i 为 1 并将其递减,通知 cv。 f2 期望 i 为 0 并增加它,通知 cv。我假设如果 f2 将 i 增加到 1,调用 cv.notify(),然后 f1 读取一个过时的 i 值(即 0),就会发生死锁,因为互斥锁和 i 之间没有内存同步,然后等待并且永远不会被唤醒向上。然后 f2 也进入睡眠状态,现在两个线程都在等待一个永远不会被通知的 cv。

我怎样才能编写这段代码,以免发生死锁?基本上我想要实现的是拥有一些由两个线程更新的原子状态。如果其中一个线程的状态不正确,我不想旋转;相反,我想在值正确时使用 cv 功能(或类似功能)来唤醒线程。

我是用g++-7用O3编译代码(虽然O0和O3都出现死锁)。

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::atomic_size_t i{0};
std::mutex mut;
std::condition_variable cv;

void f1() {
  while (1) {
    {
      std::unique_lock<std::mutex> lk(mut);
      cv.wait(lk, []() { return i.load() > 0; }); // deadlocks
    }
    --i;
    cv.notify_one();
    std::cout << "i = " << i << std::endl; // Only to avoid optimization
  }
}

void f2() {
  while (1) {
    {
      std::unique_lock<std::mutex> lk(mut);
      cv.wait(lk, []() { return i.load() < 1; }); // deadlocks
    }
    ++i;
    cv.notify_one();
    std::cout << "i = " << i << std::endl; // Only to avoid optimization
  }
}

int main() {
  std::thread t1(f1);
  std::thread t2(f2);
  t1.join();
  t2.join();
  return 0;
}

编辑:cout 只是为了避免编译器优化。

【问题讨论】:

  • —i 和 ++I 应该在 while 内。否则永远不会达到条件
  • 在值可能被另一个线程更改后打印它们。
  • 我在猜测,但我认为在一个线程更改了值但 它调用通知之后可能会发生虚假唤醒。因此,两个线程最终都可以等待变量,而不会再次调用 notify。将增量/减量移动到锁中似乎可以解决它。
  • 虽然将其移入 while 是一种解决方案,但我想知道是否可以不进行双重同步(原子已经在同步 i)。
  • @user1413793 使i 不是原子的,那么你只需要依赖互斥锁进行同步。

标签: c++ multithreading atomic c++17 condition-variable


【解决方案1】:

我认为问题在于i 的值可以更改,并且notify_one 可以在另一个线程评估return i.load() &gt; 0; 之后但在lambda 调用返回和cv 恢复等待之前的间隔内调用。这样原子变量的变化就不会被另一个线程观察到,也没有人唤醒它再次检查。这可以通过在更改变量时锁定互斥锁来解决,尽管这样做会破坏原子的目的。

【讨论】:

    【解决方案2】:

    我认为 VTT 的回答是正确的,只是想说明会发生什么。首先,代码可以改写成如下形式:

    void f1() {
       while (1) {
          {
             std::unique_lock<std::mutex> lk(mut);
             while (i == 0) cv.wait(lk);
          }
          --i;
          cv.notify_one();
       }
    }
    
    void f2() {
       while (1) {
          {
             std::unique_lock<std::mutex> lk(mut);
             while (i >= 1) cv.wait(lk);
          }
          ++i;
          cv.notify_one();
       }
    }
    

    现在,考虑以下时间线,i 最初是 0

    time step    f1:               f2:
    =========    ================= ================
            1                      locks mut
            2                      while (i >= 1) F
            3                      unlocks mut
            4    locks mut
            5    while (i == 0) T                  
            6                      ++i;
            7                      cv.notify_one();
            8    cv.wait(lk);
            9    unlocks mut(lk) 
           10                      locks mut                   
           11                      while (i >= 1) T
           12                      cv.wait(lk);
    

    实际上,f1i1 时等待。两个线程现在同时处于阻塞状态。


    解决方案是将i 的修改放入锁定部分。那么,i 甚至不需要是原子变量。

    【讨论】:

    • OP 问题中的原始符号解决了在没有谓词的情况下使用等待函数的古老困惑,所以从这个意义上说,它让事情变得更清楚,你现在展示了你真正期望的谓词,并且你不只是在等待一些信号,为了工作必须在一个while循环中。此外,可以更好地处理可选超时 (cv::wait_for())。
    • 另外,在您的场景中,while (i == 0)cv.wait(lk); 是同步的,因此它们之间的线程不能中断。
    • @stefaanv 我更新了时间线以包括互斥锁的锁定和解锁。
    • 是的,因为 ++i 没有受到保护,所以这很有效。在 while 符号中更容易显示场景。
    【解决方案3】:

    当线程不拥有互斥锁时,您调用cv.notify_one();。这可能会导致通知被发送为空。想象一下f2f1 之前开始。 f2 调用cv.notify_one();f1 尚未加入cv.wait

    获取的互斥体保证f2 要么在std::unique_lock&lt;std::mutex&gt; lk(mut) 中,要么等待通知。

    #include <atomic>
    #include <condition_variable>
    #include <iostream>
    #include <mutex>
    #include <thread>
    
    std::atomic_size_t i{0};
    std::mutex mut;
    std::condition_variable cv;
    
    void f1() {
      while (1) {
        std::size_t ii;
        {
          std::unique_lock<std::mutex> lk(mut);
          cv.wait(lk, []() { return i.load() > 0; });
          ii = --i;
          cv.notify_one();
        }
        std::cout << "i = " << ii << std::endl;
      }
    }
    
    void f2() {
      while (1) {
        std::size_t ii;
        {
          std::unique_lock<std::mutex> lk(mut);
          cv.wait(lk, []() { return i.load() < 1; });
          ii = ++i;
          cv.notify_one();
        }
        std::cout << "i = " << ii << std::endl;
      }
    }
    
    int main() {
      std::thread t1(f1);
      std::thread t2(f2);
      t1.join();
      t2.join();
      return 0;
    }
    

    顺便说一句,std::atomic_size_t i 可能是std::size_t i

    【讨论】:

    • 我认为您在通知时不需要持有互斥锁。在互斥锁被解锁之前,这不会冒被通知线程唤醒的风险吗?
    • @super 根据this:“通知线程不需要持有与等待线程持有的同一个互斥锁;实际上这样做是一种悲观,因为被通知的线程会立即再次阻塞,等待通知线程释放锁。”
    • 如果要将 i 更改为 std::size_t,则必须保护 std::cout 语句,因为它们访问 i。
    • @stefaanv 谢谢。我更新了答案。保护std::cout 是个坏主意,因此我添加了局部变量。
    【解决方案4】:

    由于 i 是原子的,因此无需使用互斥锁来保护其修改。

    f1f2 中的条件变量等待永远等待,除非发生虚假唤醒,因为从未通知条件变量。由于无法保证虚假唤醒,我建议在等待条件变量之前检查条件,并最终通知其他线程的条件变量。

    您的代码还有另一个问题。 f1f2 两个函数都不会结束。所以你的 ma​​in 函数会一直等待加入它的线程。

    【讨论】:

    • std::wait() 函数在内部执行 while(! pred) wait_without_pred()(参见 Daniel Langr 的回答),并且不结束用于演示目的的程序通常使用 ctrl-c 关闭,需要连接以保持程序处于活动状态.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-19
    • 2021-02-25
    • 1970-01-01
    • 2020-07-12
    相关资源
    最近更新 更多