1 什么是线程安全?
线程安全是指.多个线程同时对临界资源进行竞争性访问而不会造成数据的二义性(大家共享进程的大部分资源,都可以使用全局资源,但是不发生混乱)
我们都知道一个进程中的所有线程共享该进程的资源,从而使得线程间通信变得更加方便,这是它的优点.诚然我们也知道凡事都得一分为2的看,正是由于多个线程共享数据,所以容易发生冲突,可以说,这种混乱和冲突发生的风险是存在的,因此为了我们的多线程程序能够合理的运行,我们就必须保证线程安全.
2 如何保证线程安全?
实现方式 :同步与互斥
互斥: 对临界资源同一时间访问的唯一性(在我访问的时候,只能我一个人访问,别人不能访问)
同步:对临界资源访问的时序合理性(大家不要急,一个一个来)
2.1如何实现互斥
线程间互斥的实现: 互斥锁(表示当前资源是否可操作,他就是一个计数器,计数器本身就是临界资源)
互斥锁的组成 : 一个计数器 和 一个等待队列`
互斥锁的代码实现流程
1. 定义互斥锁变量 : pthread_mutex_t mutex //定义一个计数器 和一个等待队列
2. 对互斥锁变量进行初始化 : pthread_mutex_init(&mutex,&attr)
3. 对临界资源操作之前先加锁 : pthread_mutex_lock(&mutex); 若可以加锁则直接修改计数器,函数返回;否则挂起等待
5. 对临界资源操作完毕后进行解锁 :pthread_mutex_unlock(&mutex);
6. 销毁互斥锁(所有线程都不对互斥锁进行操作时,通常在程序退出的时候) :pthread_mutex_destroy(&mutex);
1 个小例子: 黄牛抢票
//黄牛抢票的栗子(加互斥锁版本)
8 pthread_mutex_t mutex;// 定一个互斥锁
9
10
11 int ticket=100;// 刚开始有100白张火车票可抢
12
13 void *yello_bull(void *arg){
14 while(1){
15 // 在对临界资源访问之前对 临界资源进行加锁
16 pthread_mutex_lock(&mutex);
17 if(ticket>0){
18 usleep(1000);
19 printf("bull %d get a ticket:%d\n",(int)arg, ticket);
20 ticket--;
21
22 }else {
23 printf("have no ticket,bull %d exit\n",(int)arg);
24 // 用户加锁之后需要在任意有可能退出线程的地方进行解锁
25 pthread_mutex_unlock(&mutex);
26 pthread_exit(NULL);
27 }
28 // 对临界资源访问完毕之后进行解锁
29 pthread_mutex_unlock(&mutex);
30 }
31
32 }
33
34
35 int main(){
36 // 创建4 个线程表示4个黄牛抢票
37
38 pthread_t tid[4];
39 //在线程创建之前进行初始化
40 pthread_mutex_init(&mutex,NULL);
41 int i;
42 for(i=0;i<4;++i){
W> 43 int ret=pthread_create(&tid[i],NULL,yello_bull,(void*)i);
44 if(ret !=0){
45 printf("thread create error");
46 return -1;
47 }
48 }
49 //等待所有线程退出
50
51 for(int i=0;i<4;++i){
52 pthread_join(tid[i],NULL);
53 }
54
55 //在不用 互斥锁时候将其销毁
56 pthread_mutex_destroy(&mutex);
57 return 0;
58
59 }
2.2 如何实现同步
线程间同步的实现: 条件变量
条件变量提供两个功能: 等待 + 唤醒
条件变量只是提供了等待和唤醒功能,具体什么时候等待与唤醒,需要用户做判断.如果可以直接访问,那么该线程就可以对临界资源直接进行操作,如果不能直接操作,则加入条件变量提供的等待队列上进行等待(让pcb处于可中断休眠状态).等待其他线程促使条件满足,然后唤醒等待队列上的线程.
条件变量的代码实现流程:
1 定义条件变量 : pthread_cond_t cond
2 条件变量初始化 : pthread_cond_init(&cond,NULL);
3 条件变量提供的等待功能(调用接口): pthread_cond_wait(&cond,&mutex)
//让线程挂起,将该线程的pcb状态修改为可中断休眠状态,现在不在运行,将这个线程加入到cond 提供的等待队列中
//pthread_cond_wait实现了三步操作:
// 1. 解锁
// 2. 休眠
// 3. 被唤醒后加锁
// 其中解锁和休眠操作必须是原子操作
4 其他线程促使条件满足之后,唤醒等待队列上的线程
pthread_cond_signal(&cond) 唤醒至少一个线程
pthread_cond_broadcast(&cond) 唤醒所有等待的线程
5 销毁条件变量pthread_cond_destory(&cond)
一个小问题 :为什么条件变量要和互斥锁一起使用 ?
线程什么时候等待,需要一个判断条件 ;而这个判断条件本身就是一个临界资源.(等待了之后,其他线程需要促使这个条件满足(
修改临界资源),因此这个临界资源的操作就需要受到保护(默认使用互斥锁来实现保护)
一个小例子: 厨师做面和顾客吃面
顾客线程来面馆吃面()访问临界资源,首先判断饭馆有没有面,如果有面,顾客线程直接去吃面(直接对临界资源进行操作).若没有面(对临界资源暂时不可直接操作),顾客线程会加入顾客线程的条件变量所提供的等待队列中.等待厨师线程来做面 ,等厨师线程做好面以后,厨师线程会唤醒正在等待的顾客线程来吃面,厨师线程自己会加入到厨师线程条件变量提供的等待队列中去,等待没面了再去做面. 由于等待队列中可能不止一个顾客线程,因此厨师线程在唤醒的时候有可能唤醒多个顾客线程, 然后,由于CPU分配资源的偶然性,其中一个比较幸运的顾客线程先拿到时间片以后,先对面进行加锁,然后去吃面了,而后拿到时间片的顾客线程只能在互斥锁提供的队列上等待,等待前面的顾客吃碗面之后,他再去加锁然后去吃面. 等第一个顾客线程吃碗面以后,唤醒厨师来做面.然后循环往复这个过程.
模拟代码如下:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int _have_noodle = 0;
pthread_mutex_t mutex;
pthread_cond_t cond_eat;
pthread_cond_t cond_cook;
void *eat_noodle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex);
while (_have_noodle == 0) {
//没有面,就不能吃面
//int pthread_cond_wait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex);
// 一直阻塞等待
//int pthread_cond_timedwait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex,
// const struct timespec *restrict abstime);
// 限时等待,等待超时后报错返回
//休眠之前应该先解锁
pthread_cond_wait(&cond_eat, &mutex);
}
//能走下来表示have_noolde==1 ,表示有面
printf("eat noodle, delicious\n");
_have_noodle = 0;
pthread_cond_signal(&cond_cook);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *cook_noodle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex);
while (_have_noodle == 1) {
//现在有面,但是没人吃,不能继续做了
pthread_cond_wait(&cond_cook, &mutex);
}
printf("cook noodle~~~ come on~~\n");
_have_noodle = 1;
pthread_cond_signal(&cond_eat);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
pthread_mutex_init(&mutex, NULL);
//int pthread_cond_init(pthread_cond_t *cond,
// const pthread_condattr_t *attr);
pthread_cond_init(&cond_eat, NULL);
pthread_cond_init(&cond_cook, NULL);
for (int i = 0; i < 4; i++) {
int ret = pthread_create(&tid1, NULL, eat_noodle, NULL);
if (ret != 0) {
printf("pthread create error\n");
return -1;
}
}
for (int i = 0; i < 4; i++) {
int ret = pthread_create(&tid2, NULL, cook_noodle, NULL);
if (ret != 0) {
printf("pthread create error\n");
return -1;
}
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex);
// pthred_cond_destory(pthread_cond_t*cond);
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&cond_cook);
pthread_cond_destroy(&cond_eat);
return 0;
}
一些小细节**?*
****条件变量的条件判断应该是一个循环判断的过程
多个顾客线程同时被唤醒,只有一个线程可以加锁,其他的顾客线程将阻塞在加锁上(而不是条件变量的等待队列上)
第一个加锁的顾客开始吃面,吃碗面后进行解锁,这时候,获取到锁的线程有可能是一个顾客线程,因为没有再次判断有没有面.因此直接它直接去吃面,但是面已经被第一个顾客线程吃掉了,因此逻辑错误.应该在加锁之后重新再次判断是否有面
不同的角色应该等待在不同的条件变量上:
在存在多个顾客线程和厨师线程的时候,若是顾客线程和厨师线程都等在同一个条件变量提供的等待队列上.如果此时厨师做了一碗面,本应该唤醒顾客线程吃面,但是由于顾客线程和厨师线程都等待在同一个条件变量所提供的等待队列上,因此 此时 有可能唤醒的还是一个厨师线程,而厨师线程由于循环判断有没有面,因为前面的厨师已经做了一碗面,所以他会陷入等待( 而顾客线程由没有被唤醒而无法吃面)