线程的同步与互斥
程序员文章站
2022-05-22 11:21:15
...
mutex(互斥量)
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内。这种情况,变量对数单个线程,其他线程无法获得这种变量
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
- 多线程并发的操作共享变量,会带来一些问题,因为毕竟不是所有的操作都是原子性的(要么不做,要么全做完,不存在中间时刻)
下面写一个程序来说明:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<pthread.h>
6
7
8 int ticket = 20;//全局变量,定义20张票
9
10 void *route(void *arg)
11 {
12 char *id = (char*)arg;
13 while(1)
14 {
15 if(ticket>0)
16 {
17 usleep(1000);
18 printf("%s sells ticket:%d\n",id,ticket);//打印当前的票和购票人
19 ticket--;
20 }
21 else
22 break;
23 }
24 }
25
26 int main()
27 {
28 pthread_t t1,t2,t3,t4;
29 //创建四个进程,四个购票人
30 pthread_create(&t1,NULL,route,"thread 1");
31 pthread_create(&t2,NULL,route,"thread 2");
32 pthread_create(&t3,NULL,route,"thread 3");
33 pthread_create(&t4,NULL,route,"thread 4");
34
35 pthread_join(t1,NULL);
36 pthread_join(t2,NULL);
37 pthread_join(t3,NULL);
38 pthread_join(t4,NULL);
39
40 }
运行结果如下:
我们发现出现了票数为负的情况,为什么会这样呢?
- 四个线程同时访问一个全局变量ticket,都进行了--操作,而--操作并不是原子操作。它会先把ticket变量从内存加载到寄存器中,更新寄存器里面的值,执行--操作,再次寄存器写回到院内从地址中,所以在此期间,很有可能多个进程同时访问该变量,导致结果异常。
- 操作不是原子的,而是对应下面三条汇编指令
- load:将共享变量ticket从内存加载到寄存器中
- update:更新寄存器里面的值,执行-1操作
- store:将新值,从寄存器写回到共享变量ticket的内存地址
有什么解决办法呢?
- 解决问题的方法也很简单,就是规定一个线程在申请到该资源用户,其他线程不能再继续申请,只能等待第一个线程释放掉该资源以后,才能进行申请。必须做到以下三点:
- 代码必须要有互斥行为:当代码加入临界区执行时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,呢吗只能允许一个线程进入该临界区
- 如果线程不在临界区执行,那么该线程不能组织其他线程进入临界区
做到这三点,本质上就是需要一把锁,Linux提供的这把锁叫互斥量
互斥:
概念:事件A与事件B在任何一次事件中不会同时发生,则称事件A和事件B互斥
线程互斥:俩个或多个线程不能同时访问同一块临界资源(共享资源),即多个线程互斥的访问同一块资源
互斥量的接口:
1.初始化互斥量(两种方法)
- 方法1,静态方法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)
参数:
- mutex:要初始化的互斥量
- attr:NULL
2.销毁互斥量
销毁互斥量需要注意:
- 使用PTHREAD_MUTEX_INITIALIZER(静态)初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destory(pthread_mutex_t *mutex);
互斥量加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用pthread_lock时,会遇到以下情况
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者其他线程同时申请互斥量,但是没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁
改进上面的买票系统:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<pthread.h>
6
7
8 int ticket = 20;
9 pthread_mutex_t lock;//创建互斥量
10
11 void *route(void *arg)
12 {
13 const void* msg = (const void*)arg;
14
15 while(1)
16 {
17 pthread_mutex_lock(&lock);//在访问ticket前上锁
18 if(ticket>0)
19 {
20 usleep(1000);
21 printf("%s sells ticket:%d\n",msg,ticket);
22 ticket--;
23 pthread_mutex_unlock(&lock);//访问结束,释放互斥锁
24 }
25 else
26 {
27 pthread_mutex_unlock(&lock);
28 break;
29 }
30 }
31 }
32
33 int main()
34 {
35 pthread_t t1,t2,t3,t4;
36
37 pthread_mutex_init(&lock,NULL);
38
39 pthread_create(&t1,NULL,route,"thread 1");
40 pthread_create(&t2,NULL,route,"thread 2");
41 pthread_create(&t3,NULL,route,"thread 3");
42 pthread_create(&t4,NULL,route,"thread 4");
43
44 pthread_join(t1,NULL);
45 pthread_join(t2,NULL);
46 pthread_join(t3,NULL);
47 pthread_join(t4,NULL);
48
49 pthread_mutex_destroy(&lock);//销毁互斥锁,一定要等待线程之后再销毁
//因为其四个线程在执行购票操作时,主线程在阻塞式等待,如果主线程先把锁销毁了,
//就会导致其余线程阻塞,因为都在等待申请锁,或者一个线程在未释放锁时,锁已经被销毁了
50 return 0;
51 }
允许结果如下:
加入互斥锁以后,我们发现票数就不会出现负的情况,但是我们同时发现,所有的票都被线程4买走了,这时我们就要引入同步的概念
同步:
- 概念:指对在一个系统中发生的事件之间进行协调,在事件上出现一致性与统一化的现象
- 线程同步: 俩个或俩个以上的线程协同的访问共享资源,即线程A访问完之后,它不急着再去申请该资源,而是让B去访问
- 为了实现多线程之间的同步行为,Linux引入了条件变量,用法和互斥相似
条件变量:
- 当一个线程互斥的访问某个变量时,他可能发现再其他线程改变状态之前,他什么也不做了
- 例如一个线程访问队列时,发现队列为空,它只能等待,只有其他线程将一个节点添加到队列中。这种情况就需要用到条件变量
条件变量函数:
1.初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *ret rict attr);
参数:
cond:要初始化的条件变量
attr:NULL
2.销毁:
int pthread_cond_destory(pthread_cond_t *cond);
3.等待条件满足:
int pthread_cond_wait(pthread_cont_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
注意:
- 要进入临界区对临界资源进行操作,所以首先要申请互斥量
- 如果发现等待条件满足,则使用wait使线程挂起等待,如果等待条件不满足,就对临界资源进行操作,释放锁
- 当对临界资源操作完成后,释放互斥量,退出临界区
在执行等待操作时,其实完成了以下事情:
- 当等待条件满足时,挂起调用他的线程
- 释放互斥量
- 当再一次被唤醒并且切换到该线程后,会重新自动获得互斥量,并在等待出继续往下执行
4.唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
当满足条件之后,就要唤醒在条件变量下等待的线程
该函数在使用时,要注意:
- 进入临界区对临界区资源进行操作,所以要先申请互斥量
- 使等待条件为假,如插入线程2向队列中插入节点
- 调用signal,如果此时有线程在等待,则唤醒他,若没有等待的线程,则该函数什么也不做
- 解锁互斥量,退出临界区
代码如下:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<pthread.h>
6
7 pthread_mutex_t lock;
8 pthread_cond_t cond;
9
10 void *route1(void *arg)
11 {
12 while(1)
13 {
14 printf("Hello world\n");
15 pthread_cond_wait(&cond,&lock);
16 printf("I am thread %d\n",(int)arg);
17 }
18 }
19
20 void *route2(void* arg)
21 {
22 while(1)
23 {
24 pthread_cond_signal(&cond);
25 printf("I am thread %d\n",(int)arg);
26 sleep(1);
27 }
28 }
29
30 int main()
31 {
32 pthread_t t1,t2;
33
34 pthread_mutex_init(&lock,NULL);
35 pthread_cond_init(&cond,NULL);
36
37 pthread_create(&t1,NULL,route1,(void*)1);
38 pthread_create(&t2,NULL,route2,(void*)2);
39
40 pthread_join(t1,NULL);
41 pthread_join(t2,NULL);
42
43 pthread_mutex_destroy(&lock);
44 pthread_cond_destroy(&cond);
45 return 0;
46 }
运行结果如下:
俩个线程协同工作,互不影响
为什么pthread_cond_wait需要互斥量?
- 条件等待时线程间同步的一种手段,如果只有一个线程,且条件不满足,那么它一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使得原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
- 条件不会无缘无故的满足,要使其满足必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据
上一篇: java多线程-死锁和死锁避免
下一篇: opencv画圆与棋盘图