【问题标题】:Why can't thread2 acquire the lock if thread1 acquires the lock repeatedly?如果thread1多次获取锁,为什么thread2不能获取锁?
【发布时间】:2019-07-22 09:22:47
【问题描述】:

thread1:锁定、休眠、解锁; thread2:加锁、解锁;

但是thread2永远无法获取锁,thread1反复获取锁。

我在Ubuntu 16.04(gcc 5.4)下尝试了很多次

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

std::thread t1([&mutex]() {
    while (true) {
        pthread_mutex_lock(&mutex);
        std::cerr << "thread 1" << std::endl;
        sleep(1);
        pthread_mutex_unlock(&mutex);
    }
});

std::thread t2([&mutex]() {
    while (true) {
        pthread_mutex_lock(&mutex);
        std::cerr << "thread 2" << std::endl;
        pthread_mutex_unlock(&mutex);
    }
});

t1.join();
t2.join();

线程 1 线程 1 线程 1 线程 1 线程 1 线程 1 线程 1 ...

【问题讨论】:

  • 请发布一个完整的示例,其他人可以使用它来尝试重现您的结果。
  • 听起来像是正常的饥饿问题。如果没有看到一些现实的minimal reproducible example,就很难说出它为什么会发生。
  • 另外,考虑到您使用std::thread 和 lambda,为什么不改用标准 C++ 锁定原语?
  • 但是thread2永远无法获得锁的现象也很奇怪 -- 真的,真的不是。想想如果sleep 不存在会发生什么——你会期望看到thread 1 的长期运行和thread 2 的长期运行,而不是交替thread 1 thread 2 thread 1 thread 2,因为线程2 只会在以下情况下获得控制权它会在 thread1 释放它的确切瞬间尝试锁定,反之亦然——因此,一旦线程获得锁定,它将倾向于反复重新锁定它。 sleep 只是使线程 2大大不太可能在正确的时间尝试锁定。
  • @trentcl :是的,但假设调度程序是默认的SCHED_OTHER 并且没有手动调整友好度,只有人为的示例实际上不会产生。诚然,由于临界区 inside 的睡眠,这可能是一个人为的例子,希望这是从来没有人真正做过的事情。

标签: c multithreading locking mutex


【解决方案1】:

这个问题有个名字。它被称为 starvation,当任何线程长时间保持互斥锁锁定时,这通常会成为问题。您示例中的两个线程中的每一个都尝试使互斥锁始终保持锁定所有。 (是的,他们每次在循环中解锁互斥锁,但随后他们在下一条指令中重新锁定它。)

解决饥饿问题的最佳方法是,不要让互斥锁保持不必要的锁定时间。您示例中的线程之一在互斥锁被锁定时调用sleep()。在任何实际程序中,这几乎总是是个坏主意。

另一种解决方法是使用所谓的“公平”互斥锁。公平互斥锁的所有权总是授予等待时间最长的线程。它的效率低于常规类型的互斥锁,但它可以轻松解决一些 饥饿问题。不幸的是,可能没有任何标准方法(或根本没有任何方法)可以在任何给定平台上获得公平的互斥锁。


仅供参考:在您的示例中发生的情况是,线程 1 从 sleep() 调用中唤醒并释放互斥锁。一旦互斥锁被释放,两个线程之间就开始竞争,看哪个线程接下来会得到它。不幸的是,当枪响时,线程 2 被阻塞等待互斥锁。操作系统将线程 2 移动到“运行队列”,它必须等待 CPU 运行。同时,线程 1 已经在运行。线程 1 再次抓取互斥锁,然后线程 2 很快唤醒,发现互斥锁被锁定,然后返回等待。

【讨论】:

  • 如果从线程1中移除睡眠,那么两个线程将被更公平地唤醒;即使thread1先获取了锁,thread2之后仍然可以获取锁。与 sleep 相比,thread2 在这种情况下根本无法获取锁。怎么解释?
  • @user3015856 sleep() 不是真正的问题。真正的问题是,任何一个线程在释放互斥锁后所做的下一件事是再次锁定互斥锁。这使其他线程没有真正的竞争机会,因为获取不公平的互斥锁并不仅仅发生:线程本身必须执行指令以使它发生。但是“饿死”的线程此时并没有运行,而刚刚释放互斥锁的线程已经在运行,所以刚刚释放互斥锁的线程总是获胜。
  • 感谢您的解释。但是仍然无法解释如果从thread1中删除sleep,为什么thread1在thread1一次获胜后不能一次次获胜(正如你所说,thread1释放锁定后会立即再次锁定)?
  • @user3015856 线程 2 在线程 1 运行时被阻塞。 “阻塞”意味着操作系统必须做一些实际的工作才能让它再次运行。当线程 1 释放锁时,操作系统立即执行该工作。它所做的只是记下线程 2 现在是“可运行的”这一事实。同时,线程 1 继续运行。线程 1 所做的下一件事是再次锁定锁。 最终操作系统完成了唤醒线程2的任务,但是当它唤醒时,发现锁又被锁定了。在那一点上它什么也做不了,只能回去等待。
  • 如果从线程1中移除睡眠,结果可能是这样的:线程1(重复20次),线程2(重复10次),线程1(重复15次),...但是在线程1中睡眠,结果是 thread2 从未获得锁,这在我看来非常奇怪。也许@trentcl 的评论很有用:睡眠只是让线程 2 在正确的时间尝试锁定的可能性大大降低
【解决方案2】:

pthread_mutex_t 不保证在 lock() 操作上阻塞的线程处于先进先出等待队列中。
只要thread1 解锁互斥体,它就会继续下一次迭代并再次锁定它,成功!

对于调度程序而言,阻止thread1 并唤醒thread2做更多的工作,而不仅仅是让thread1unlock() 操作之后运行。

这个问题是相关的: Implementing a FIFO mutex in pthreads

【讨论】:

  • 感谢您指出我们经常期望它像FIFO一样的错误,FIFO互斥体的实现也很有趣。
猜你喜欢
  • 2019-12-17
  • 2021-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多