线程安全:多个线程执行流对临界资源的不安全争抢操作
实现:如何让线程之间安全对临界资源进行操作就是同步与互斥
互斥:同一时间临界资源的唯一访问性
mutex(互斥量)
- ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来⼀些问题。
我们先来看一个例子:操作共享变量的售票系统。
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
int ticket = 100;
void* route(void* arg)
{
int id = (int)arg;
while(1)
{
if(ticket > 0)
{
usleep(1000);
printf("pthread %d -> %d\n", id, ticket);
ticket--;
}
else
break;
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i, ret;
for(i = 0; i < 4; ++i)
{
ret = pthread_create(&tid[i], NULL, route, (void*)i);
if(ret != 0)
return -1;
}
for(i = 0; i < 4; ++i)
pthread_join(tid[i], NULL);
return 0;
}
很明显,ticket都卖完了,还有线程在抢票。
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段
- --ticket操作本⾝就不是⼀个原⼦操作
--操作并不是原⼦操作,⽽是对应三条汇编指令:
- load:将共享变量ticket从内存加载到寄存器中
- update: 更新寄存器⾥⾯的值,执⾏-1操作
- store:将新值,从寄存器写回共享变量ticket的内存地址。
要解决以上问题,需要做到三点:
- 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
- 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
- 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口:
初始化互斥量
初始化互斥量有两种⽅法:
- ⽅法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
销毁互斥量
销毁互斥量需要注意:
- 使⽤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<unistd.h>
#include<pthread.h>
#include<string.h>
int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
int id = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
usleep(1000);
printf("pthread %d -> %d\n", id, ticket);
ticket--;
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i, ret;
pthread_mutex_init(&mutex, NULL);
for(i = 0; i < 4; ++i)
{
ret = pthread_create(&tid[i], NULL, route, (void*)i);
if(ret != 0)
return -1;
}
for(i = 0; i < 4; ++i)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
很明显,加锁之后线程安全得到了保证。至于只有一个线程访问,这与时间片有关。
死锁:
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和
解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确
定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态
的情况下占用资源。因此,对资源的分配要给予合理的规划。
避免死锁:银行家算法,死锁检测算法
同步:对临界资源操作的时序可控性
条件变量:等待、唤醒
条件变量一共提供了两个功能,一个是等待,一个是唤醒
对于一个外部条件进行判断,如果条件满足则继续操作;如果条件不满足怎等待
为了能够让程序继续操作,需要其他执行流修改条件,是满足条件,并唤醒对方
这里所说的外部条件,条件变量不会提供是我们用户设置的判断依据
条件变量
- 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量
条件变量函数
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict 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 mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后⾯详细解释
唤醒等待
唤醒所有等待
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒单个等待
int pthread_cond_signal(pthread_cond_t *cond);
示例:
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
pthread_cond_t sale;
pthread_cond_t eat;
pthread_mutex_t mutex;
int have_noodle = 0;
void* sale_noodle(void* arg)
{
int id = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(have_noodle == 1)
pthread_cond_wait(&sale, &mutex);
printf("pthread %d create noodle!!\n", id);
have_noodle = 1;
//生产出来后通知买方便面的人
pthread_cond_signal(&eat);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* eat_noodle(void* arg)
{
while(1)
{
//因为have_noodle的操作也是一个临界资源的操作,因此需要被
//保护,使用互斥锁进行保护
pthread_mutex_lock(&mutex);
if (have_noodle == 0)
{
//因为等待时间不确定,因此有可能会浪费很多等待时间
//因此使用环境变量提供的死等操作,但是这个死等需要能够
// 被唤醒,这样的话,一旦方便面生产出来直接唤醒我们的死
//等,不会浪费多余的等待时间
//防止不满足条件陷入休眠,没有解锁,对方无法获取锁,没
//办法生产方便面,因此需要解锁,
//但是解锁和休眠必须是原子操作
pthread_cond_wait(&eat, &mutex);
//被唤醒,这时候可以继续吃面,并且修改条件,但是条件是
//临界资源,因此需要加锁,
// pthread_cond_wait整体操作
// 解锁-》休眠-》被唤醒后加锁(但是这不是一个阻塞操作,
//而是直接计数器置0)
}
printf("eat noodle!! good!!\n");
have_noodle = 0;
pthread_mutex_unlock(&mutex);
//吃完之后通知一下卖方便面的
pthread_cond_signal(&sale);
}
}
int main()
{
pthread_t tid1, tid2;
int ret;
//条件变量初始化
pthread_cond_init(&eat, NULL);
pthread_cond_init(&sale, NULL);
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&tid1, NULL, sale_noodle, (void*)1);
if(ret != 0)
return -1;
ret = pthread_create(&tid1, NULL, sale_noodle, (void*)2);
if(ret != 0)
return -1;
ret = pthread_create(&tid2, NULL, eat_noodle, NULL);
if(ret != 0)
return -1;
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//条件变量销毁
pthread_cond_destroy(&eat);
pthread_cond_destroy(&sale);
pthread_mutex_destroy(&mutex);
return 0;
}
为什么pthread_ cond_ wait 需要互斥量?
- 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好的通知等待在条件变量上的线程。
- 条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。
按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变量上不就⾏了,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&unlock);
- 由于解锁和等待不是原⼦操作。调⽤解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是⼀个原⼦操作。
- nt pthread_ cond_ wait(pthread_ cond_ t *cond,pthread_ mutex_ t * mutex); 进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
条件变量使⽤规范
- 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);