【问题标题】:Do I have a risk to put a thread in deadlock with std::condition_variable in C++?我是否有风险在 C++ 中使用 std::condition_variable 使线程陷入死锁?
【发布时间】:2020-07-01 11:08:33
【问题描述】:

这是一个更理论的问题。我见过std::condition_variable 的几个例子,它似乎让线程进入睡眠状态,直到满足条件。这是某种种族标志,是有道理的。

但似乎如果一个变量总是被改变,那么一个线程可能会被唤醒检查条件并再次进入睡眠状态,因为当谓词为真时它没有跟上捕捉情况,所以我可能会将线程放入“昏迷”。

那么,std::condition_variable在我经常更换的这种情况下使用它不安全吗?

【问题讨论】:

  • 请在问题中包含一个示例。
  • 您的意思是像向线程发出信号表明它应该做某事,然后您在“做某事”和“什么都不做”之间频繁切换?只是不要这样做。给线程发一次信号
  • condition_variable 的等待在您通知时唤醒。目的,就是你等到条件满足。因此,一旦满足条件(或至少有机会),您应该随后通知它。也有无规律的唤醒——所以要确保你有一个真实的唤醒条件。您还需要互斥锁来保护您更改的变量。
  • 这是可能的,但是设置条件变量的线程做错了,或者线程没有从条件中唤醒是一个有效的结果
  • 如果你把“理论问题”变成一个关于代码的具体问题会很好。目前的答案或多或少:没有条件变量不是不安全的。它们可能会被错误地使用,但几乎任何事情都是如此。

标签: c++ multithreading


【解决方案1】:

这就是为什么您在使用条件变量时也必须使用互斥锁。我认为您错过了线程开始等待并同时锁定互斥锁的事实。改变条件的线程在改变条件时也必须锁定互斥锁。

线程 1:

Lock mutex
Check condition (it's false)
Unlock mutex and start waiting (these happen at the exact same time)
Finish waiting and lock mutex
Check condition (it's true)
Unlock mutex

线程 2:

Lock mutex
Change condition
Unlock mutex
Notify condition variable

当条件已经为真时,线程 1 不可能等待。因为当互斥锁被锁定时条件不能改变,并且互斥锁直到线程 1 已经在等待时才会解锁。

有可能在线程 2 释放互斥体之后,但在线程 2 通知它之前,线程 1 可能会虚假唤醒,看到条件为真,做一些其他事情,然后再次开始等待。然后线程 2 会通知条件变量,线程 1 会将其视为虚假唤醒。

【讨论】:

    【解决方案2】:

    您似乎在谈论某种“活锁”。死锁是一个或多个线程在等待释放锁时没有进展的情况。 Livelock 是类似的,但进程的状态在不断变化,但仍然没有进展。我们还可以谈论“实际上是活锁的”,即线程没有足够的机会取得足够的进展。

    对于观察者来说,活锁和实际活锁通常看起来很像真正的死锁。

    您应该设计您的程序逻辑以避免活锁。它可以是不平凡的。例如,许多形式的锁都不“公平”。也就是说,当多个线程正在等待一个已释放的锁时,首先请求它的锁并不能保证接下来会收到它。

    事实上,许多操作系统锁本质上是不公平的,例如,将锁分配给更容易唤醒(例如加载到内核并挂起)而不是更难(从内核卸载并需要重新加载以恢复执行)的线程。

    这个问题没有提供很多细节,因此很难诊断情况。 如果某个特定线程(或线程类)需要优先级,您可能会引入一个标志,告诉低优先级线程在优先级线程正在等待(并且可以运行)时不获取锁,例如 (c && !(p && priority_waiting)) 其中c 是逻辑低优先级线程的条件,p 是优先级线程的逻辑条件。

    您当然应该避免线程等待潜在瞬态条件的逻辑。 假设您有一些监视器线程,它每 1000 个周期产生一次输出。像(cycles%1000 == 0) 这样的等待条件很容易错过计数器点击。他们应该更有可能像(cycles-lcycles >=0) 这样lcycles 是监视器上次恢复处理时的周期数。这确保了监视器通常会获得一个它可能(出于实际目的)几乎永远不会捕获的锁。

    在该示例中,线程正在等待 (a) 获取锁的机会和 (b) 一些瞬态条件。两者同时发生的情况很少见,并且线程可能被活锁或实际上被活锁并且进展不足,

    简而言之,确保线程在条件通过时恢复,而不是在条件完全满足时恢复。

    你可以引入一个严格的队列来让线程轮流。除非文档对公平做出明确承诺,否则不要假设这是您所拥有的。

    【讨论】:

      【解决方案3】:

      这就是为什么condition_variable 总是与锁(mutex)结合使用。

      生产者和消费者逻辑都是在持有相同锁的情况下执行的,因此任何共享数据(例如队列、某些条件标志等)都只能由一个线程访问一次。

      在释放锁时通知condition_variable,等待线程在唤醒时获取锁。所以实际上,锁是从生产者转移到消费者线程的,不可能在消费者有机会看到之前设置条件和取消设置,iff em> 至少有一个其他线程正在等待它。

      换句话说,condition_variable::notify_one保证唤醒至少一个等待线程 (condition_variable::wait),如果有的话。当然,生产者应该在调用notify_one之前或之后释放锁,让等待的线程继续。

      【讨论】:

      • 其实最好在调用nofiy_one/notify_all之前释放锁。通知不需要持有互斥锁,这样做会导致刚刚通知的线程在尝试获取锁时立即再次阻塞。
      猜你喜欢
      • 2010-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-01
      • 1970-01-01
      • 2012-07-27
      • 2018-11-06
      相关资源
      最近更新 更多