Linux线程同步 互斥量与条件变量
线程同步常用的方法
- 使用互斥量保护共享资源,对共享资源进行互斥访问,共享资源是指不同线程共有的变量、端口等,如全局变量、共享内存、文件读写端口等;
- 使用条件变量进行线程同步,通过发送条件变量信号和接收条件变量信号来同步线程;
- 信号量与条件变量类似,但条件变量仅能用于线程同步,而信号量既能用于线程同步又能用于进程同步,并且信号量可以实现信号计数,而条件变量不行;
互斥量
默认情况下多线程的调度机制是SCHED_OTHER 循环时间分享策略,在该策略下可通过修改nice值来修改各线程占用时间片的权重,但是仍然没有优先级的机制(具有优先级的实时线程调度策略有SCHED_RR和SCHED_FIFO)。在多线程运行时,任何线程在代码运行的任何位置都可能由于时间片到期而被其他线程打断。
若多个线程需要操作同一资源,则极有可能两个线程在同一时刻竞争同一资源,这种竞争会造成共享资源数据错乱等危害。
互斥量可保证不同线程对同一资源进行互斥访问。
互斥量的创建
#include <pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //静态创建一个互斥量并初始化
pthread_mutex_t * mtx = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t)); //动态创建互斥量
pthread_mutex_init(mtx, NULL); //初始化动态互斥量 注意:不能重复初始化已经初始化过后的互斥量
pthread_mutex_destory(mtx); //在用户程序free掉malloc的mtx前,需要使用该函数摧毁该互斥量,而静态创建的互斥量不需要摧毁
/*函数原型*/
/*
* @Description: 初始化动态创建的信号量,已经初始化过后的互斥量不能再此初始化。
* @Para : pthread_mutex_t * mutex 需要初始化的互斥量地址
* const pthread_attr_t * attr 所创建的互斥量属性,默认设置为NULL
*
* @return : 成功返回0,失败返回正数
**/
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
/*
* @Description: 摧毁动态创建的互斥量
* @Para : pthread_mutex_t * mutex 需要摧毁的互斥量地址
*
* @return : 成功返回0,失败返回正数
**/
int pthread_mutex_destroy(pthread_mutex_t * mutex);
注意:
- 已经pthread_mutex_destroy过后的动态互斥量,可以通过pthread_mutex_init再次初始化;
- 初始化一个已经初始化过的互斥量将导致未定义的行为,应当避免这一操作。
互斥量的加锁与解锁
#include <pthread.h>
/*
* @Description: 对一个互斥量加锁(解锁)
* @Para : pthread_mutex_t * mutex 需要加锁(解锁)的互斥量地址
*
* @return : 成功返回0,失败返回正数
**/
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
//例: thread one 和 thread two 共同操作一个全局变量int huge;
pthread_mutex_t huge_mutex = PTHREAD_MUTEX_INITIALIZER;
//thread one 代码
...
pthread_mutex_lock(&huge_mutex);
huge = huge + 1;
pthread_mutex_unlock(&huge_mutex);
...
//thread two 代码
...
pthread_mutex_lock(&huge_mutex);
huge = huge + 2;
pthread_mutex_unlock(&huge_mutex);
...
两个线程在对同一个全局变量或其它共享资源进行操作时,需要使用互斥量对共享变量进行保护,防止在操作变量过程中线程被打断,而另外一个线程又修改变量造成计算错乱。
当有互斥量保护时,thread one 使用huge前加锁huge_mutex,此时如果thread one时间片结束,运行thread two,则thread two试图加锁huge_mutex发现该互斥量以被加锁无法使用,则thread two只能挂起,则马上又可回到thread one继续运行。
加锁解锁互斥量时必须注意:
- 任何时刻,只有一个线程可以锁定该互斥量,尝试去锁定一个已经锁定的互斥量,会让该线程挂起,直到有其它线程解锁该互斥量;
- 一个线程锁定互斥量,则该线程成为互斥量的所有者,只有所有者才能够解锁该互斥量;
- 不能对未锁定的互斥量进行解锁,不能解锁由其它线程锁定的互斥量;
互斥量的死锁
//实例:
线程A 线程B
1. pthread_mutex_lock(mutex1); 1. pthread_mutex_lock(mutex2);
2. pthread_mutex_lock(mutex2); 2. pthread_mutex_lock(mutex1);
当线程A和线程B都成功锁定第一个互斥量时,两线程继续运行都会造成两线程无限的等待。这种由于互斥量使用不当,造成线程无限等待的情况称为死锁。
解决上述死锁的方法是:定义互斥量的层级关系,多个线程操作一组互斥量时,总按照相同的顺序对这组互斥量进行操作;
注意:一个线程对同一个信号量连续加锁两次,也会造成该线程死锁。
条件变量
条件变量允许一个线程通知另外一个等待该条件变量的线程,即等待信号线程挂起直至获得另外一个线程的通知去执行某些操作,类似于信号量,但两者也有区别;
注意:
- 条件变量的发送信号是没有计数的,如果发送时没有接收对象,则该发送的条件变量将被忽略。但信号量有计数功能,能记录发送信号的次数;
- 条件变量必须配合互斥量使用。
条件变量的创建
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态创建条件变量
pthread_cond_t * cond = (pthread_cond_t *)malloc(sizeof(pthread_cond_t)); //动态创建条件变量
pthread_cond_init(cond, NULL); //初始化动态条件变量 注意:不能重复初始化已经初始化过后的条件变量
pthread_cond_destory(cond); //在用户程序free掉malloc的cond前,需要使用该函数摧毁该条件变量,而静态创建的条件变量不需要摧毁
/*函数原型*/
/*
* @Description: 初始化动态创建的条件变量,已经初始化过后的条件变量不能再次初始化。
* @Para : pthread_cond_t * cond 需要初始化的条件变量地址
* const pthread_condattr_t * attr 所创建的条件变量属性,默认设置为NULL
*
* @return : 成功返回0,失败返回正数
**/
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr);
/*
* @Description: 摧毁动态创建的条件变量
* @Para : pthread_cond_t * cond 需要摧毁的动态变量地址
*
* @return : 成功返回0,失败返回正数
**/
int pthread_cond_destroy(pthread_cond_t * cond);
注意:
- 已经pthread_cond_destroy过后的动态条件变量,可以通过pthread_cond_init再次初始化;
- 初始化一个已经初始化过的动态变量将导致未定义的行为,应当避免这一选项。
通知条件变量
#include <pthread.h>
/*
* @Description: 通知至少一条(所有)等待条件变量的线程
* @Para : pthread_cond_t * cond 要通知的条件变量的地址
* @return : 成功返回0,失败返回正数
**/
int pthread_cond_signal(pthread_cond_t * cond);
int pthread_cond_broadcast(pthread_cond_t * cond);
注意:
- pthread_cond_signal能够唤醒至少一条等待条件变量的线程, 一般只唤醒一条。pthread_cond_broadcast是将所有等待该条件变量的线程唤醒;
等待条件变量
/*
* @Description: 等待一个条件变量,等待条件变量时会阻塞线程
* @Para : pthread_cond_t * cond 等待条件变量的地址
* pthread_mutex_t * mutex 等待条件变量时需要释放的互斥量
* @return : 成功返回0,失败返回正数
**/
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
/*
* @Description: 等待一个条件变量,等待条件变量时会阻塞线程,并且等待一段时间没有
* 收到条件变量通知,则返回并报错
* @Para : pthread_cond_t * cond 等待条件变量的地址
* pthread_mutex_t * mutex 等待条件变量时需要释放的互斥量
* const struct timespec * abstime 等待直到该绝对时间
* @return : 成功返回0,失败返回正数
**/
int pthread_cond_timewait(pthread_cond_t * cond, pthread_mutex_t * mutex, const struct timespec * abstime);
注意:
-
等待的过程有如下三步,对互斥量的操作都是该函数自动完成的:
- 解锁互斥量;
- 挂起线程,直至另一个线程发送条件变量通知;
- 接收到条件变量后,重新锁定互斥量,继续后续程序运行。
之所以要如此操作是为了在线程等待条件变量时,可以解锁该线程的互斥量,释放共享资源给其他需要的线程使用;
-
用户在设计代码时,在pthread_cond_wait接收到条件变量,继续运行相应操作前,一定要重新检测后续程序可运行的条件。因此,通常如下例设计代码。
//例如:等待一个队列不为空时,取出队列参数并处理
while(queue.size() == 0)
pthread_cond_wait(&cond, &mutex);
temp = queue.front();
queue.pop();
//解释:当一开始进入时,可能队列为空,然后等待条件变量通知,线程挂起。然后另外一个
//线程向队列中填入值,并发送条件变量(或许压根没有填队列就发送了条件变量)。由于线
//程处理是有延迟的,当被阻塞的线程再次唤醒时,可能队列中值已经被其他线程取走,所以,
//在线程被条件变量阻塞而重新唤醒时,一定要重新判断其后续的执行条件,不满足时继续阻塞。