【问题标题】:Question for threadsafe queue from three easy pieces三个简单的线程安全队列问题
【发布时间】:2020-06-30 07:06:02
【问题描述】:

以下是书中给出的代码来说明一个问题: 当有 1 个生产者 Tp 和 2 个消费者 Tc1,Tc2 时,以下代码将不起作用。 原因是当 Tp 将数据放入队列时,它唤醒了 Tc1,在 Tc1 运行之前,Tc2 潜入并取出唯一的元素,从而清空队列。然后 Tc1 开始调用 get() ,因为队列为空,所以会抛出异常。并且书籍建议将“if(count ==0)”替换为“while(count == 0)”,这样可以确保 Tc1 的状态在它运行之前是需要的。

只有当 Tc2 确实有办法在 Tc1 之前潜入时,我才有意义。如果 Tp 调用 notify 来唤醒 Tc1,则 Tc1 可能会抢到锁,在这种情况下 Tc2 将无法潜入并做任何事情。如果 Tc1 确实未能抓住锁而 Tc2 确实如此,那么 Tc1 应该回到睡眠状态,直到它再次收到通知。在那种情况下,它永远不会进入 get() 行。

所以我的问题是:如果 Tc1 抓住锁,为什么 Tc2 会在 Tc1 之前潜入并做任何事情?

cond_t cond;
2 mutex_t mutex;
3
4 void *producer(void *arg) {
5     int i;
6     for (i = 0; i < loops; i++) {
7         Pthread_mutex_lock(&mutex); // p1
8         if(count == 1) // p2
9             Pthread_cond_wait(&cond, &mutex); // p3
10        put(i); // p4
11        Pthread_cond_signal(&cond); // p5
12        Pthread_mutex_unlock(&mutex); // p6
13    }
14 }
15
16 void *consumer(void *arg) {
17     int i;
18     for (i = 0; i < loops; i++) {
19         Pthread_mutex_lock(&mutex); // c1
20         if(count == 0) // c2
21             Pthread_cond_wait(&cond, &mutex); // c3
22         int tmp = get(); // c4
23         Pthread_cond_signal(&cond); // c5
24         Pthread_mutex_unlock(&mutex); // c6
25         printf("%d\n", tmp);
26     }
27 }

【问题讨论】:

  • Pthread_cond_signal 至少唤醒一个线程,因此两个线程都可能被唤醒
  • @AlanBirtles 如果我使用来自 c++ lib 的条件变量并调用 notify_one 会有所不同吗?能保证只唤醒一个吗?
  • @AlanBirtles 也是,即使 Pthread_cond_signal 唤醒 2,应该只有一个成功锁定,另一个应该进入睡眠?
  • 不明白,如果两个都可以以这种方式抢到锁,那么这里加锁的目的是什么?
  • 是的,一次只会唤醒一个,但是一旦第一个完成执行并释放锁,第二个就可以获取锁并开始执行

标签: c++ concurrency locking


【解决方案1】:

pthread_cond_wait 释放互斥体并在“一个原子动作”中进入睡眠状态。但是,被唤醒并重新获取互斥锁并不会自动发生(如果pthread_cond_broadcast 可能会唤醒多个线程,这应该如何工作?)。

pthread_cond_wait 保证在成功返回后,互斥体将被锁定并归调用线程所有。但是一个刚刚被唤醒的线程不能因为互斥锁当前被其他线程锁定而重新进入睡眠状态。再次考虑pthread_cond_broadcast 场景 - 多个线程被唤醒并竞争互斥锁,但显然只有一个线程可以抓住它 - 其他线程将立即重新进入睡眠状态。那将完全违背pthread_cond_broadcast 的目的。

因此可能会发生 Tc1 被唤醒,但 它可以获取互斥锁 Tc2 潜入并获取锁。在这种情况下,Tc1 必须等待 Tc2 解锁互斥锁,然后才能观察到一个空队列。

更新
当等待条件变量的线程被唤醒时,它会尝试获取互斥锁。 pthread_cond_wait 只会在线程拥有互斥锁后返回!所以考虑这种情况(队列最初是空的):

  • Tc1:尝试弹出一个项目,但队列为空,因此它调用pthread_cond_wait 并阻塞条件变量
  • Tp:推送一个项目并向条件变量发出信号
  • Tc1:唤醒,但尚未获取互斥锁
  • Tc2:潜入并抓住互斥锁
  • Tc1:现在再次阻塞(仍然在 pthread_cond_wait 内),但这次 _ 在互斥体上_

在这种情况下,Tc1 必须等待 Tc2 释放互斥锁,但此时 Tc2 已经移除了最后一项,所以一旦 Tc1 从pthread_cond_wait 返回,它会找到一个空队列。

这只是一种可能的情况,甚至有 Tc1 和 Tc2 甚至不必竞争互斥锁的情况:

  • Tc1:尝试弹出一个项目,但队列为空,因此它调用pthread_cond_wait 并阻塞条件变量
  • Tp:推送一个项目并向条件变量发出信号
  • Tc2:偷偷溜进去弹出那个物品
  • Tc1:现在才醒来 - 发现一个空队列

但是这些比赛并不是这个实现的唯一问题。 pthread_cond_wait 可以有虚假唤醒,这意味着即使条件变量没有发出信号,线程也可以唤醒:

可能会发生来自pthread_cond_timedwait()pthread_cond_wait() 函数的虚假唤醒。由于来自pthread_cond_timedwait()pthread_cond_wait() 的返回并不暗示该谓词的任何值,因此应根据此类返回重新评估该谓词。

所以基本上你应该总是pthread_cond_wait返回时重新评估你正在等待的条件,如果条件不满足,你可能想再次调用pthread_cond_wait重新进入睡眠状态.

【讨论】:

  • 如你所说,Tc1 唤醒,如果 Tc2 抢到锁,则必须等待 Tc2。那它怎么能观察到空队列呢?那行代码是在等待之后...如果它没有锁,它应该没有办法跳过等待??
  • 不,Tc1 在获取锁后从等待中返回。但是由于 Tc2 可能已经潜入,所以队列现在可能是空的,导致下面的调用失败。
  • 这很奇怪吗?通过返回,您实际上是在说 Tc1 在没有锁的情况下继续进行,这对我来说没有意义吗?首先,为什么它可以在没有锁的情况下进行?其次,与C++条件变量相比,等待发出信号时唤醒,只有在它可以抢到锁时才继续,否则它会返回睡眠,这个等待不同吗?您能否更详细地说明为什么在没有锁的情况下等待返回?
  • "由于各种竞争条件,您还需要简单地使用 notify_all() 而不是多次调用 notify_one()。请记住,wait() 会自动解锁互斥锁并等待条件变量,并且wait() 仅在线程收到条件变量通知后成功重新锁定互斥锁后才返回。”这就是这个答案所说的。
猜你喜欢
  • 1970-01-01
  • 2010-10-07
  • 2020-10-07
  • 2017-06-14
  • 1970-01-01
  • 2012-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多