多线程的同步与互斥
学习https://blog.csdn.net/daaikuaichuan/article/details/82950711
一、同步与互斥的概念
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:
都需要访问/使用同一种资源;
多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
同步】:
是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
【互斥】:
是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
二、互斥锁(同步)
在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。
【特点】
原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
【操作流程】
- 在访问共享资源后临界区域前,对互斥锁进行加锁;
- 在访问完成后释放互斥锁导上的锁。在访问完成后释放互斥锁导上的锁;
- 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
【函数】
|
【实例1】
//使用互斥量解决多线程抢占资源的问题 |
【实例2】
|
三、条件变量(同步)
【特点】
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步 的一种机制,主要包括两个动作:
一个线程等待”条件变量的条件成立”而挂起;
另一个线程使 “条件成立”(给出条件成立信号)。
【原理】
条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。
【条件变量的操作流程如下】
初始化:init()或者pthread_cond_tcond=PTHREAD_COND_INITIALIER;属性置为NULL;
等待条件成立:pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真 timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait);
激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
清除条件变量:destroy;无线程等待,否则返回EBUSY清除条件变量:destroy;无线程等待,否则返回EBUSY
【实例】
生产者和消费者模型,生产者向队列中插入数据,消费者则在生产者发出队列准备好(有数据了)后接收消息,然后取出数据进行处理。实现的关键点在以下几个方面:
- 生产者和消费者都对条件变量的使用加了锁
- 消费者调用pthread_cond_wait,等待队列是否准备好的信息,注意参数有两个,一个是pthread_cond_t,另外一个是pthread_mutex_t.
代码:
|
【疑问】
为什么pthread_cond_wait需要加锁??
pthread_cond_wait中的mutex用于保护条件变量,调用这个函数进行等待条件的发生时,mutex会被自动释放,以供其它线程(生产者)改变条件,pthread_cond_wait中的两个步骤必须是原子性的(atomically,万恶的APUE中文版把这个单词翻译成了『自动』,误人子弟啊),也就是说必须把两个步骤捆绑到一起:
- 把调用线程放到条件等待队列上
- 释放mutex
不然呢,如果不是原子性的,上面的两个步骤中间就可能插入其它操作。比如,如果先释放mutex,这时候生产者线程向队列中添加数据,然后signal,之后消费者线程才去『把调用线程放到等待队列上』,signal信号就这样被丢失了。
如果先把调用线程放到条件等待队列上,这时候另外一个线程发送了pthread_cond_signal(我们知道这个函数的调用是不需要mutex的),然后调用线程立即获取mutex,两次获取mutex会产生deadlock.
在生产者线程中修改条件时为什么要加mutex??
如果不这么做信号可能会丢失,看下面的例子:
Thead A Thread B |
在while判断之后向队列中插入数据,虽然已经有数据了,但线程A还是调用了pthread_cond_wait等待下一个信号到来。。
消费者线程中判断条件为什么要放在while中??
while (workq == NULL) |
我们把while换成if可不可以呢?
if (workq == NULL) |
答案是不可以,一个生产者可能对应着多个消费者,生产者向队列中插入一条数据之后发出signal,然后各个消费者线程的pthread_cond_wait获取mutex后返回,当然,这里只有一个线程获取到了mutex,然后进行处理,其它线程会pending在这里,处理线程处理完毕之后释放mutex,刚才等待的线程中有一个获取mutex,如果这里用if,就会在当前队列为空的状态下继续往下处理,这显然是不合理的。
signal到底是放在unlock之前还是之后??
void |
如果先unlock,再signal,如果这时候有一个消费者线程恰好获取mutex,然后进入条件判断,这里就会判断成功,从而跳过pthread_cond_wait,下面的signal就会不起作用;另外一种情况,一个优先级更低的不需要条件判断的线程正好也需要这个mutex,这时候就会转去执行这个优先级低的线程,就违背了设计的初衷。
void |
如果把signal放在unlock之前,消费者线程会被唤醒,获取mutex发现获取不到,就又去sleep了。浪费了资源.但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。
所以在Linux中推荐使用这种模式。