来自知乎的pthread_cond_wait为什么总是带着mutex

来自https://www.zhihu.com/question/24116967?q=linux%20%E5%A4%9A%E7%BA%BF%E7%A8%8B%20%E8%99%9A%E5%81%87%E5%94%A4%E9%86%92%20%E9%97%AE%E9%A2%98%3F%E6%88%91%E6%80%8E%E4%B9%88%E8%A7%A3%E9%87%8A%E4%B8%8D%E5%90%8C%E8%82%AF%E5%AE%9A%E5%93%AA%E9%87%8C%E4%B8%8D%E5%AF%B9%E4%BA%86

吴志强 ,还是喜欢在晚上写代码

——————更新,勘误——————

昨天晚上回到宿舍谷歌到了官方文档(在机房的时候谷歌挂了,怎么搜都搜不到……),发现之前的解释有个错误,即 @高大月 同学指出的“原子”语义。我结合昨天写的测试代码再补充一下。

我在原来的答案中,有这样的代码:

    pthread_mutex_unlock(mtx);
    pthread_cond_just_wait(cv);
    pthread_mutex_lock(mtx);

事实上,上面三行代码的并不是pthread_cond_wait(cv, mtx)的内联展开。其中第一行和第二行必须“原子化”,而第三行是可以分离出去的(之所以要把第三行放在里面的原因可以参见原来的答案)。

那么为什么第一行和第二行不能分离呢?这是因为必须得保证:如果线程A先进入wait函数(即使没有进入实际的等待状态,比如正在释放mtx),那么必须得保证其他线程在其之后调用的broadcast必须能够将线程A唤醒。

所以,把原来答案中的代码再贴一遍:

// 线程A,条件测试
pthread_mutex_lock(mtx);        // a1
while(pass == 0) {              // a2
    pthread_mutex_unlock(mtx);  // a3
    pthread_cond_just_wait(cv); // a4
    pthread_mutex_lock(mtx);    // a5
}
pthread_mutex_unlock(mtx);

// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);   // b1
pass = 1;                  // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv);   // b4

如果执行序列是:a1, a2, a3, b1, b2, b3, b4, a4,那么线程A将不会被唤醒。而a3在线程B之前执行,这意味着wait函数是在signal之前调用的,所以不满足上文提到的保证。

解决办法:

  1. 先将线程附加到等待队列
  2. 释放mutex
  3. 进入等待

感兴趣的同学的可以看下源码(pthread_cond_wait.c),附加到等待队列这个操作是加锁的,所以可以保证之前发起的signal不会错误得唤醒本线程,而之后发起的signal必然唤醒本线程。

因此,下面的代码是绝对不会出错的:

// 线程A,条件测试
pthread_mutex_lock(mtx);        // a1
while(pass == 0) {              // a2
    pthread_cond_wait(cv, mtx); // a3
}
pthread_mutex_unlock(mtx);      // a4

// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);   // b1
pass = 1;                  // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv);   // b4

如果线程A先运行,那么执行序列必然是:a1, a2, a3, b1, b2, b3, b4, a4。
如果线程B先运行,那么执行序列可能是:b1, b2, b3, b4, a1, a2, a4
也可能是:b1, b2, b3, a1, a2, a3, b4, a4

所以,如果是我设计pthread API,那么我会添加一个pthread_cond_unlock_and_wait函数,伪代码如下:

int pthread_cond_wait(cv, mtx) {
    int ret = pthread_cond_unlock_and_wait(cv, mtx);
    pthread_mutex_lock(mtx);
    return ret;
}

// 线程A,条件测试
pthread_mutex_lock(mtx);
if (pass == 0)
    pthread_cond_unlock_and_wait(cv, mtx);
else
    pthread_mutex_unlock(mtx);

// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);   // b1
pass = 1;                  // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv);   // b4

这样的好处在于:如果我们可以保证没有虚假唤醒(即不需要while循环测试条件),那么我们可以将线程A的代码改成上述形式,这样无论怎样都只需要执行一次pthread_mutex_unlock()函数,而之前的版本至少需要执行两次。

—————— 原来的答案——————

感谢大家的回答!
看了之后,我获得了启发,突然觉得这或许是跟条件变量的通常用法有关。

首先需要明白两点:

  • wait()操作通常伴随着条件检测,如:

    while(pass == 0)
        pthread_cond_wait(...);
    
  • signal*()函数通常伴随着条件改变,如:

    pass = 1;
    pthread_cond_signal(...)
    

由于此两处都涉及到变量pass,所以为了防止Race Condition,必须得加锁。所以代码会变成下面这样:

// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0)
    pthread_cond_wait(...);
pthread_mutex_unlock(mtx);

// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(...);

然后,我们假设wait()操作不会自动释放、获取锁,那么代码会变成这样:

// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0) {
    pthread_mutex_unlock(mtx);
    pthread_cond_just_wait(cv);
    pthread_mutex_lock(mtx);
}
pthread_mutex_unlock(mtx);

// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(cv);

久而久之,程序员发现unlock, just_wait, lock这三个操作始终得在一起。于是就提供了一个pthread_cond_wait()函数来同时完成这三个函数。

另外一个证据是,signal()函数是不需要传递mutex参数的,所以关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。

所以我的结论是:传递的mutex并不是为了防止wait()函数内部的Race Condition!而是因为调用wait()之前你总是获得了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),并且这个mutex在你调用wait()之前必须得释放掉,调用wait()之后必须得重新获取。

所以,pthread_cond_wait()函数不是一个细粒度的函数,却是一个实用的函数。

时间: 2024-10-05 05:11:48

来自知乎的pthread_cond_wait为什么总是带着mutex的相关文章

[转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程

一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http://blog.csdn.net/locape/article/details/6040383 http://www.cnblogs.com/liuweijian/archive/2009/12/30/1635888.html 一.什么是多线程? 当我自己提出这个问题的时候,我还是很老实的拿着操作系

线程同步之条件变量使用手记

由来: 最近一直在想怎么高效率的在IO线程接收到数据时通知逻辑线程(基于线程池)工作的问题,像网络编程的服务器模型的一些模型都需要用到这个实现,下面我这里简单的罗列一个多线程的网络服务器模型 半同步/半异步(half-sync/half-async): 许多餐厅使用 半同步/半异步 模式的变体.例如,餐厅常常雇佣一个领班负责迎接顾客,并在餐厅繁忙时留意给顾客安排桌位,为等待就餐的顾客按序排队是必要的.领班由所有顾客"共享",不能被任何特定顾客占用太多时间.当顾客在一张桌子入坐后,有一个

线程同步:何时互斥锁不够,还需要条件变量?

http://www.blogjava.net/fhtdy2004/archive/2009/07/05/285519.html 线程同步:何时互斥锁不够,还需要条件变量? 很显然,pthread中的条件变量与Java中的wait,notify类似 假设有共享的资源sum,与之相关联的mutex 是lock_s.假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++.那么只用mutex足够了.程序员只要确保每个线程操作前,取得lock,然后sum++,再unlock即可.每个

主线程和子线程的同步控制

一个线程的结束有两种途径,一种是象我们以下的样例一样.函数结束了.调用它的线程也就结束了.还有一种方式是通过函数pthread_exit来实现.另外须要说明的是,一个线程不能被多个线程等待,也就是说对一个线程仅仅能调用一次pthread_join.否则仅仅有一个能正确返回.其它的将返回ESRCH 错误. 在Linux中,默认情况下是在一个线程被创建后.必须使用此函数对创建的线程进行资源回收,可是能够设置Threads attributes来设置当一个线程结束时.直接回收此线程所占用的系统资源.具

JDFS:一款分布式文件管理实用程序第一篇(线程池、epoll、上传、下载)

一 前言 截止目前,笔者在博客园上面已经发表了3篇关于网络下载的文章,这三篇博客实现了基于socket的http多线程远程断点下载实用程序.笔者打算在此基础上开发出一款分布式文件管理实用程序,截止目前,已经实现了 服务端/客户端 的上传.下载部分的功能逻辑.涉及到的知识点包括线程池技术.linux epoll并发技术.上传.下载等.JDFS的下载功能的逻辑部分与笔者前几篇关于JWebFileTrans(JDownload)比较类似.如果读者对socket网络下载不熟悉或者是只对下载功能感兴趣,请

【转】Linux下的多线程编

作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/原文链接:http://www.cnblogs.com/gnuhpc/archive/2012/12/07/2807484.html 本文作者: 姚继锋 (2001-08-11 09:05:00) 黄鹏程(2009-03-13) converse (2009-01-15) 1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传

【转】Linux下的多线程编程

1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程.现在,多线程技术已经被许多操作系统所支持,包括Windows也包括Linux.  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题.  使用多线程的理由之一是和进程相比,它是一种非

条件变量signal与unlock的顺序

编写同步队列时,有用到条件变量,对操作队列的线程进行同步.当队列为空时,允许get线程挂起,直到add线程向队列添加元素并通过唤醒条件变量,get线程继续向下运行.条件变量在多线程程序中用来实现“等待->唤醒”逻辑常用的方法.条件变量要和互斥量相联结,以避免出现条件竞争:一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件.使用条件变量进行同步时,通常以如下方式进行编码: 1 pthread_mutex_t mutex; 2 pthread_cond_t cond;

4.锁--并行编程之条件变量(posix condition variables)

在整理Java LockSupport.park()的东东.看到了个"Spurious wakeup".又一次梳理下. 首先来个<UNIX环境高级编程>里的样例: [cpp] view plaincopy #include <pthread.h> struct msg { struct msg *m_next; /* ... more stuff here ... */ }; struct msg *workq; pthread_cond_t qready =