为什么线程需要同步与互斥机制
- ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来⼀些问题。
假设两个线程读写相同变量时,线程A读取变量然后给这个变量赋予一个新的值,但写操作需要两个存储周期。当线程B在这两个写周期读取这个变量时,可能会得到不一致的值。为了避免这个问题,就需要互斥,同一时间只允许一个线程访问该变量。
假设一个场景,现有100张车票,新建4个线程连续不断进行买票,直到票售完。我们先看代码与运行结果:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<pthread.h> int ticket = 100; void* route(void* arg) { char* id = (char*)arg; while(1) { if(ticket > 0) { usleep(1000); printf("%s get a ticket:ticket:%d\n",id,ticket); ticket--; } else { break; } } } int main() { pthread_t t1,t2,t3,t4; pthread_create(&t1,NULL,route,"thread 1"); pthread_create(&t2,NULL,route,"thread 2"); pthread_create(&t3,NULL,route,"thread 3"); pthread_create(&t4,NULL,route,"thread 4"); pthread_join(t1,NULL); pthread_join(t2,NULL); pthread_join(t3,NULL); pthread_join(t4,NULL); return 0; }
运行结果截图:
我们可以看到,ticket数量已经变为-2,这显然不合常理
这是因为ticket--不是一个原子操作,它对应3条汇编指令
- load:将共享变量ticket从内存加载到寄存器中
- update: 更新寄存器⾥⾯的值,执⾏-1操作
- store:将新值,从寄存器写回共享变量ticket的内存地址
所以,thread1获取ticket的数量时,thread3还没有把ticket--,所以才会出现-1,-2张票。
要解决以上问题需要解决以下3点:
- 代码必须要有互斥行为:当代码进⼊临界区执⾏时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执行,那么只能允许⼀个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要实现以上要求则需要一把锁,Linux将这把锁叫互斥量。
初始化互斥量
初始化互斥量有两种⽅法:
- ⽅法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- ⽅法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值:成功返回0,失败返回错误号
_t *restrict attr);参数:mutex:要初始化的互斥量attr: NULL 销毁互斥量
销毁互斥量需要注意:
- 使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
- 不要销毁⼀个已经加锁的互斥量
- 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
int pthread_mutex_destroy(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调⽤会陷⼊阻塞,等待互斥量解锁。
改进上面的售票系统代码:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<pthread.h> int ticket = 100; pthread_mutex_t mutex; void* route(void* arg) { char* id = (char*)arg; while(1) { pthread_mutex_lock(&mutex); if(ticket > 0) { usleep(1000); printf("%s get a ticket:ticket:%d\n",id,ticket); ticket--; pthread_mutex_unlock(&mutex); } else { pthread_mutex_unlock(&mutex); break; } } } int main() { pthread_mutex_init(&mutex,NULL); pthread_t t1,t2,t3,t4; pthread_create(&t1,NULL,route,"thread 1"); pthread_create(&t2,NULL,route,"thread 2"); pthread_create(&t3,NULL,route,"thread 3"); pthread_create(&t4,NULL,route,"thread 4"); pthread_join(t1,NULL); pthread_join(t2,NULL); pthread_join(t3,NULL); pthread_join(t4,NULL); pthread_mutex_destroy(&mutex); return 0; }
运行结果:
条件变量
- 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要用到条件变量。
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest rict attr); 参数: cond:要初始化的条件变量 attr: NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mute x); 参数: cond:要在这个条件变量上等待 mutex:互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
简单案例,首先不加条件变量:
void* rout1(void* arg) { while(1) { printf("I am thread A!\n"); } } void* rout2(void* arg) { while(1) { printf("I am thread B!\n"); } } int main() { pthread_t t1,t2; pthread_create(&t1,NULL,rout1,NULL); pthread_create(&t2,NULL,rout2,NULL); pthread_join(&t1,NULL); pthread_join(&t2,NULL); return 0; }
运行结果:
可见,程序执行后,要么打印大量A,要么大量打印B。若加上条件变量,则会呈现出A、B交替打印的状态
pthread_cond_t cond; pthread_mutex_t mutex; void* rout1(void* arg) { while(1) { pthread_cond_wait(&cond,&mutex); printf("I am thread A!\n"); } } void* rout2(void* arg) { while(1) { printf("I am thread B!\n"); pthread_cond_signal(&cond); } } int main() { pthread_t t1,t2; pthread_cond_init(&cond,NULL); pthread_mutex_init(&mutex,NULL); pthread_create(&t1,NULL,rout1,NULL); pthread_create(&t2,NULL,rout2,NULL); pthread_join(t1,NULL); pthread_join(t2,NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
为什么pthread_cond_wait需要互斥量?
- 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满足,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要用互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。