多线程同步分析
理解这部分概念,需要先知道以下几个名词解释,我也去认真查了查,防止理解有误哈。
- 顺序执行:一个应用程序由若干程序段组成,每个程序段完成特定的功能,它们在执行时,都需要按照某种先后次序顺序执行,仅当前一程序执行完后,才运行后一程序段。
- 并发执行:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
- 并行执行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
- 同步:对于资源来看,当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。对于一个接口来看,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
- 异步:异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。(摘自百度文库:https://baike.baidu.com/item/%E5%B9%B6%E5%8F%91/11024806)
任何事物都不是完美的,一个新事物的引进,解决了某些迫切问题,但也会带来某些其他问题,只是带来的问题可控。多线程的并发执行虽然提升了系统资源的利用率,提高了系统的性能,但是并发执行也带来了新的问题——资源抢占。
办公室里面经常会出现的问题:职员A正在打印文件,B也要打印,他们都在自己的电脑上进行操作,结果B先打印完,但是A去拿了文件,没看内容直接交到了老板的手中。这就是资源抢占,要是能有个流程,比如A先打印,打印完了拿出文件,然后告诉B你可以打印了,这样就解决了资源抢占的问题。
代码中解决资源抢占问题的办法就称为线程同步。(“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合)
同步的方式主要有三种:互斥量(互斥锁、Mutex)、条件变量(Cond)、信号量(Semaphore)等
- 互斥量(互斥锁、Mutex)
互斥锁以排他方式防止共享数据被并发访问。互斥锁是一个二元变量,只有锁定(禁止1)和解锁(允许0)两种状态,互斥锁可以看作是特殊意义的全局变量,因为在同一时刻只有一个线程能够对互斥锁进行操作。
将某个共享资源与某个特定互斥锁在逻辑上绑定,即要申请该资源必须先获取锁。对该共享资源的访问操作如下:
- 首先申请互斥锁,如果该互斥锁处于锁定状态,默认阻塞当前线程;如果处于解锁状态,则申请到该锁并立即占有该锁,使锁处于锁定状态防止其他线程访问该资源。
- 只有锁定该互斥锁的线程才能释放该互斥锁,其他线程试图释放操作无效。
功能 | 函数 |
初始化互斥锁 | pthread_mutex_init |
阻塞申请互斥锁 | pthread_mutex_lock |
非阻塞申请互斥锁 | pthread_mutex_trylock |
释放互斥锁 | pthread_mutex_unlock |
销毁互斥锁 | pthread_mutex_destroy |
/*
*互斥锁使用方法
*第一步:声明互斥锁
*第二部:初始化互斥锁
*第三部:申请互斥锁
*第四部:释放互斥锁
*第五部:销毁互斥锁
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
//剩余票数,全局变量,共享数据
int gRemainTicket = 100;
pthread_mutex_t mut;//第一步:声明互斥锁
void* process1(void* args){
pthread_detach(pthread_self());
int buyNum = (int)(*((int*)args));
int flag = 0;
while((buyNum--)>0){
pthread_mutex_lock(&mut);//第三步:申请互斥锁
if( gRemainTicket > 0 ){
sleep(1);
gRemainTicket--;//关键资源
printf("窗口1----------------还剩%d张票 \n",gRemainTicket);
}else{
flag = 1;
}
pthread_mutex_unlock(&mut);//第四步:释放互斥锁
sleep(1);//之后执行这段代码的时间内,process2、process3将有权利处理gRemainTicket
if(flag == 1) {
printf("窗口1----------------票已卖完 \n");
break;
}
}
pthread_mutex_unlock(&mut);
return NULL;
}
void* process2(void* args){
pthread_detach(pthread_self());
int buyNum = (int)(*((int*)args));
int flag = 0;
while((buyNum--)>0){
pthread_mutex_lock(&mut);//第三步:申请互斥锁
if( gRemainTicket > 0 ){
sleep(1);
gRemainTicket--;//关键资源
printf("窗口2----------------还剩%d张票 \n",gRemainTicket);
}else{
flag = 1;
}
pthread_mutex_unlock(&mut);//第四步:释放互斥锁
sleep(1);//之后执行这段代码的时间内,process1、process3将有权利处理gRemainTicket
if(flag == 1) {
printf("窗口2----------------票已卖完 \n");
break;
}
}
pthread_mutex_unlock(&mut);
return NULL;
}
void* process3(void* args){
pthread_detach(pthread_self());
int buyNum = (int)(*((int*)args));
int flag = 0;
while((buyNum--)>0){
pthread_mutex_lock(&mut);//第三步:申请互斥锁
if( gRemainTicket > 0 ){
sleep(1);
gRemainTicket--;//关键资源
printf("窗口3----------------还剩%d张票 \n",gRemainTicket);
}else{
flag = 1;
}
pthread_mutex_unlock(&mut);//第四步:释放互斥锁
sleep(1);//之后执行这段代码的时间内,process1、process2将有权利处理gRemainTicket
if(flag == 1) {
printf("窗口3----------------票已卖完 \n");
break;
}
}
pthread_mutex_unlock(&mut);
return NULL;
}
int main(){
pthread_mutex_init(&mut,NULL);//第二步:初始化互斥锁
printf("jack----------------1 \n");
pthread_t t1;
pthread_t t2;
pthread_t t3;
int buyNum = 10;
printf("jack----------------2 \n");
pthread_create(&t1,NULL,process1,(void*)&buyNum);
printf("jack----------------3 \n");
pthread_create(&t2,NULL,process2,(void*)&buyNum);
printf("jack----------------4 \n");
pthread_create(&t3,NULL,process3,(void*)&buyNum);//创建三个线程
printf("jack----------------5 \n");
pthread_join(t1,NULL);
printf("jack----------------6 \n");
pthread_join(t2,NULL);
printf("jack----------------7 \n");
pthread_join(t3,NULL);
printf("jack----------------8 \n");
pthread_mutex_destroy(&mut);//第五步:销毁互斥锁
return 0;
}
运行环境:ubuntu14.04,平台自带gcc编译器
运行结果:
root@ubuntu:/home/jack#
root@ubuntu:/home/jack# make
gcc -o test test1.c -lpthread
root@ubuntu:/home/jack# ./test
jack----------------1
jack----------------2
jack----------------3
jack----------------4
jack----------------5
jack----------------6
jack----------------7
窗口1----------------还剩99张票
窗口2----------------还剩98张票
窗口3----------------还剩97张票
窗口1----------------还剩96张票
窗口2----------------还剩95张票
窗口3----------------还剩94张票
窗口1----------------还剩93张票
窗口2----------------还剩92张票
窗口3----------------还剩91张票
窗口1----------------还剩90张票
窗口2----------------还剩89张票
窗口3----------------还剩88张票
窗口2----------------还剩87张票
窗口1----------------还剩86张票
窗口3----------------还剩85张票
窗口2----------------还剩84张票
窗口1----------------还剩83张票
窗口3----------------还剩82张票
窗口2----------------还剩81张票
窗口1----------------还剩80张票
窗口3----------------还剩79张票
窗口1----------------还剩78张票
窗口3----------------还剩77张票
窗口2----------------还剩76张票
窗口1----------------还剩75张票
窗口3----------------还剩74张票
窗口2----------------还剩73张票
窗口1----------------还剩72张票
窗口3----------------还剩71张票
窗口2----------------还剩70张票
jack----------------8
root@ubuntu:/home/jack#
测试程序下载地址:https://download.csdn.net/download/sleeping_sunshine/11578692
提问:为什么jack----------------6 、jack----------------7 顺序执行了,而jack----------------8最后执行?(如果您看了线程前面的文章的话,相信这个问题,稍加思考就能明白)
-
条件变量
如果有多个线程使用的互斥锁,将浪费大量系统资源。举个例子:
职工1、职工2、职工3......职工100,都要去上卫生间,但是现在就一个位置,那就只能先到先上呀,其他人先回去干活等会再来看看,你说说急不急吧,过会就要来看下,过会就要来看下,浪费了大量的工作时间,现在假如卫生间里面的职工出来了,并且大吼一声我结束了,那其他人是不是都知道了,不用每次都要去看看。高铁上卫生间指示工作就很典型,通过不同颜色的灯来指示。
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。
在项目中会经常使用条件变量来控制某一资源,通常在“生产者-消费者模型中”使用比较多,比如说遥控器的按键,硬件层接收到遥控器的硬件码,上传到中间层,一个线程接收用户的按键(生产者),接收到之后需要转换为应用码,通知某个应用程序(appmanager任务)来取(消费者),这样上层应用不会时不时的来查看下有没有按键来,只需要等通知就行了。
功能 | 函数 |
初始化条件变量 | int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr); |
等待条件变量 | int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex); |
等待条件变量到指定时间 | int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const structtimespec * abstime); |
通知等待条件变量的单个线程 | int pthread_cond_signal(pthread_cond_t *cv); |
通知等待条件变量的所有线程 | int pthread_cond_broadcast(pthread_cond_t *cv); |
销毁条件变量 | int pthread_cond_destroy(pthread_cond_t *cv); |
/*
*条件变量使用方法
*第一步:声明条件变量
*第二部:初始化互斥锁
*第三部:申请互斥锁
*第四部:释放互斥锁
*第五部:销毁互斥锁
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
//剩余票数,全局变量,共享数据
int key_value = -1;
pthread_mutex_t mut;//第一步:声明互斥锁
pthread_cond_t hasKey;//第一步:声明条件变量
void* produce_key(void* args){
pthread_detach(pthread_self());//至线程为可分离状态,运行结束自动释放资源
while(1){
pthread_mutex_lock(&mut);//第三步:申请互斥锁
key_value = rand()/100;//产生100以内的随机数
printf("produce_key is %d.\n", key_value);
pthread_mutex_unlock(&mut);//第四步:释放互斥锁
pthread_cond_signal(&hasKey);//第五步:通知线程有key来
usleep(50*1000);//50*1000微秒=50ms
}
return NULL;
}
void* consume_key(void* args){
pthread_detach(pthread_self());
while(1){
pthread_mutex_lock(&mut);//第三步:申请互斥锁
while( -1 == key_value ){//建议将常量放在左侧
pthread_cond_wait(&hasKey, &mut);
}
printf("consume_key is %d.\n", key_value);
pthread_mutex_unlock(&mut);//第四步:释放互斥锁
usleep(50*1000);//50*1000微秒=50ms,这段时间给生产者制造产生随机数
}
return NULL;
}
int main(){
pthread_mutex_init(&mut,NULL);//第二步:初始化互斥锁
pthread_cond_init(&hasKey,NULL);//第二步:初始化条件变量
printf("jack----------------1 \n");
pthread_t t1;
pthread_t t2;
int buyNum = 10;
printf("jack----------------2 \n");
pthread_create(&t1,NULL,produce_key,NULL);
printf("jack----------------3 \n");
pthread_create(&t2,NULL,consume_key,NULL);
printf("jack----------------4 \n");
pthread_join(t1,NULL);
printf("jack----------------5 \n");
pthread_join(t2,NULL);
printf("jack----------------6 \n");
pthread_mutex_destroy(&mut);//第六步:销毁互斥锁
pthread_cond_destroy(&hasKey);//第六步:销毁条件变量
return 0;
}
运行环境:ubuntu14.04,平台自带gcc编译器
运行结果:
root@ubuntu:/home/jack# make
gcc -o test test2.c -lpthread
root@ubuntu:/home/jack#
root@ubuntu:/home/jack# ./test
jack----------------1
jack----------------2
jack----------------3
jack----------------4
jack----------------5
produce_key is 18042893.
consume_key is 18042893.
produce_key is 8469308.
consume_key is 8469308.
produce_key is 16816927.
consume_key is 16816927.
consume_key is 16816927.
produce_key is 17146369.
consume_key is 17146369.
produce_key is 19577477.
produce_key is 4242383.
consume_key is 4242383.
produce_key is 7198853.
consume_key is 7198853.
produce_key is 16497604.
consume_key is 16497604.
produce_key is 5965166.
consume_key is 5965166.
produce_key is 11896414.
consume_key is 11896414.
consume_key is 11896414.
produce_key is 10252023.
produce_key is 13504900.
consume_key is 13504900.
produce_key is 7833686.
consume_key is 7833686.
测试程序下载地址:https://download.csdn.net/download/sleeping_sunshine/11579414
提问:为什么在函数“consume_key()”中,第四步:释放互斥锁pthread_mutex_unlock(&mut)之后要usleep(50*1000)?
-
信号量(Semaphore)
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
信号量和互斥锁(mutex)的区别:
-
互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区;
- 信号量可作用于线程/进程之间,而互斥锁只作用于线程之间。
功能 | 函数 |
初始化信号量 | sem_init(sem_t *sem,int pshared,unsigned int value); |
加锁-- | sem_wait(sem_t *sem); |
解锁++ | sem_post(sem_t* sem); |
销毁信号量 | sem_destroy(sem_t* sem); |
举个例子:
/*
*信号量使用方法
*第一步:声明生产者信号量
*第二步:初始化信号量
*第三步:加锁
*第四步:解锁
*第五步:销毁信号量
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
//剩余票数,全局变量,共享数据
int key_value = -1;
sem_t producer_sem;//第一步:声明生产者信号量
sem_t customer_sem;//第一步:声明消费者信号量
void* produce_key(void* args){
pthread_detach(pthread_self());//至线程为可分离状态,运行结束自动释放资源
while(1){
sem_wait(&producer_sem);//第三步:加锁,查看有无可用生产者信号量
key_value = rand()/100;//产生100以内的随机数
printf("produce_key is %d.\n", key_value);
sem_post(&customer_sem);//第四步:解锁,提醒消费者消费,释放自己占用的信号量
usleep(50*1000);//50*1000微秒=50ms
}
return NULL;
}
void* consume_key(void* args){
pthread_detach(pthread_self());
while(1){
sem_wait(&customer_sem);//第三步:加锁,查看有无可用消费者信号量
printf("consume_key is %d.\n", key_value);
sem_post(&producer_sem);//第四步:解锁,提醒生产者生产,释放自己占用的信号量
usleep(50*1000);//50*1000微秒=50ms
}
return NULL;
}
int main(){
sem_init(&producer_sem,0,2);//第二步:初始化信号量
sem_init(&customer_sem,0,2);//第二步:初始化信号量
printf("jack----------------1 \n");
pthread_t t1;
pthread_t t2;
printf("jack----------------2 \n");
pthread_create(&t1,NULL,produce_key,NULL);
printf("jack----------------3 \n");
pthread_create(&t2,NULL,consume_key,NULL);
printf("jack----------------4 \n");
pthread_join(t1,NULL);
printf("jack----------------5 \n");
pthread_join(t2,NULL);
printf("jack----------------6 \n");
sem_destroy(&producer_sem);//第五步:销毁信号量
sem_destroy(&customer_sem);//第五步:销毁信号量
return 0;
}
运行环境:ubuntu14.04,平台自带gcc编译器
运行结果:
root@ubuntu:/home/jack# make
gcc -o test test3.c -lpthread
root@ubuntu:/home/jack# ./test
jack----------------1
jack----------------2
jack----------------3
jack----------------4
jack----------------5
produce_key is 18042893.
consume_key is 18042893.
produce_key is 8469308.
consume_key is 8469308.
produce_key is 16816927.
consume_key is 16816927.
produce_key is 17146369.
consume_key is 17146369.
produce_key is 19577477.
consume_key is 19577477.
consume_key is 19577477.
produce_key is 4242383.
consume_key is 4242383.
produce_key is 7198853.
produce_key is 16497604.
consume_key is 16497604.
produce_key is 5965166.
consume_key is 5965166.
produce_key is 11896414.
consume_key is 11896414.
produce_key is 10252023.
consume_key is 10252023.
produce_key is 13504900.
consume_key is 13504900.
produce_key is 7833686.
consume_key is 7833686.
consume_key is 7833686.
produce_key is 11025200.
produce_key is 20448977.
consume_key is 11025200.
测试程序下载地址:https://download.csdn.net/download/sleeping_sunshine/11579771
有没有可能pthread_create(&t1,NULL,produce_key,NULL)、pthread_create(&t2,NULL,consume_key,NULL)执行完后,先打印线程2的log,如果可能,打印什么内容?