线程
使用的数据都属于局部变量
,局部变量
存储在线程的栈帧
中,这种变量属于单个线程
,其他线程无法获得者种变量
变量需要在线程间共享
,这样的变量称为共享变量(一般指全局变量)
,可以通过数据之间的共享来实现线程之间的交互
并发的操作共享变量
,一定会导致问题
的,互斥量
就是为了解决这种问题
的共享的资源
就叫做临界资源
内部访问临界资源
的代码叫做临界区
互斥
保证有且只有一个执行流
进入临界区
,访问临界资源
,互斥量通常是对临界资源起到保护作用
同步
是在互斥的基础上
,按照某种特定的次序去访问临界资源
只有两种状态
,要么完成
,要么没有完成
我们可以以一个多线程实现的简单售票系统
来说明互斥量
是什么:
#include
#include
#include
using namespace std;
int ticket = 100;
void *route(void* arg)
{
char* name = (char*)arg;
while(1)
{
if(ticket > 0)
{
usleep(1000);
//没有对临界资源加锁,会产生问题
cout << name << " buy ticket:" << ticket << endl;
--ticket;
}
else
{
break;
}
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1, NULL, route, (void*)"thread 1 ");
pthread_create(&t2, NULL, route, (void*)"thread 2 ");
pthread_create(&t3, NULL, route, (void*)"thread 3 ");
pthread_create(&t4, NULL, route, (void*)"thread 4 ");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
可以发现票数竟然出现负数了,这个售票系统肯定是存在问题的,原因在与我们并没有将临界资源ticket
保护起来,假设当ticket=1
时,进程1判断条件成立进入if中,但是还没有执行--ticket
的时候,它的时间片到了,OS切换到下一个线程,此时ticket
依然等于1,该线程依然会进入到if条件中,此时就会导致问题了,本来上一个线程在运行时就没有票了,但是这个线程拿到却依然有票。这个问题我们就可以通过加上互斥量来解决,将临界资源保护起来。
出现错误的原因在于:
可以并发的切换到其他线程
usleep
这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
--ticket
根本就不是一个原子性
操作,站在汇编的角度去看这个操作,其实对于的是三条汇编:load(将ticket从内存加载到寄存器)、updata(更新寄存器中的值,执行-1操作)、store(将新值从寄存器写回共享变量ticket的内存地址中)
。解决该错误的方法:
--ticket
这个非原子性的操作时必须要有互斥
行为,当一个线程进入临界区时,不允许其他线程进入
线程没有在临界区
中执行,那么该线程不能阻止其他线程进入临界区
多个线程
同时要执行临界区中的代码,并且临界期没有线程正在执行,那么只允许一个线程
进入该临界区
上述的三点其实就是互斥量
。互斥量的本质其实是一把锁
,也叫做互斥锁
,也可以理解为一个二元信号量
。互斥量是最基本的同步形式,它用来保护临界区资源
,以保证任何时刻只有一个线程在执行其中的代码
:
//上锁
pthread_mutex_lock()
...
//临界区(只允许有一个线程执行)
...
//解锁
pthread_mutex_unlock()
//非临界区,可以允许多个线程同时执行
Posix互斥锁被声明为pthread_t
类型的变量。初始化互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr _t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:属性,先设置为NULL
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex销毁哪一个信号量
销毁信号量时要注意:
PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量
不需要销毁已经加锁
的互斥量
销毁
的互斥量,要确保
后面不会
有线程再尝试加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码
加锁时需要注意: 如果互斥量处于未锁
状态,lock函数
会将该互斥量锁定
,同时返回成功
。如果其他线程已经锁定
互斥量,或者
存在其他线程时申请互斥量
,但没有竞争到互斥量
,那么pthread_ lock
调用会陷入阻塞
,等待互斥量解锁
。
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码
我们可以根据上边学习到的互斥量
对售票系统进行修改,在临界区
加上互斥锁
,以保护临界资源
,不被多个线程重入
。
修改代码位于我的github:https://github.com/hansionz/Linux_Code/tree/master/pthread/ticket
当一个线程互斥
的访问某个变量
时,它可能发现在其他线程改变状态
之前,它什么也做不了。例如,一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。
条件变量
使我们可以睡眠等待某种条件
出现,条件变量
是利用线程间共享的全局变量进行同步
的一种机制。主要包括两个动作:一个线程等待"条件变量的条件成立"
而挂起;另一个线程使"条件成立"(给出条件成立信号)
。为了防止竞争
,条件变量的使用总是和一个互斥锁
结合在一起。
条件变量其实是以pthread_cond_t
为类型的变量,
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)
参数:
cond:表示要初始化的条件变量
此操作对应概念中的一个线程为等待条件变量的条件成立而挂起
。
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mute x);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
pthread_cond_wait
的功能包括两步解锁和挂起等待
既然涉及到解锁
,那我们就必须要存在互斥量
对其操作线程间同步
的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作
,改变共享变量,使原先不满足
的条件变得满足
,并且友好的通知
等待在条件变量上的线程不会无缘无故
的突然变得满足
了,必然会牵扯到共享数据的变化
。所以一定要用互斥锁来保护
。没有互斥锁
就无法安全的获取和修改
共享数据对于pthread_cond_wait
的两个操作是否是必须的呢?这两个操作是否可以分离开来操作:我们可以想到先上锁,发现条件不满足,解锁,然后在条件变量下等待
,这样是否可行?
lock();
while(条件为假){
unlock();
///在解锁之后,等待之前,条件可能已经满⾜足,信号已经发出,但是该信号可能被错过
wait();
lock();
}
unlock();
解锁和等待
不是原子操作
。调用解锁之后,pthread_ cond_ wait
之前,如果已经有其他线程获取到互斥量
,摒弃条件满足,发送了信号,那么pthread_ cond_ wait
将错过
这个信号,可能会导致线程永远阻塞
在这个pthread_ cond_ wait
。所以解锁和等待必须是一个原子操作。此操作对应概念中的一个线程使得条件成立(给出条件成立的信号)
。
//广播唤醒在该条件变量等待下的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
/唤醒在该条件变量下等待的一个线程(队列的第一个)
int pthread_cond_signal(pthread_cond_t *cond);
等待条件代码
应该这么写:pthread_mutex_lock();
while(条件为假)
pthread_cond_wait();
//条件成立则返回
//新的线程被唤醒会自动的重新申请锁
修改条件
pthread_cond_unlock();
对于上边的条件判断为什么要使用while而不是if呢?
上边使用while而不是if
的原因在与防止假唤醒
。假唤醒是指在多核处理器上,pthread_cond_signal
函数不仅仅只会唤醒一个线程,而是可能会唤醒多个线程,在唤醒的这多个线程中,可能只有1个是满足条件的。所以我们需要在pthread_cond_wait
函数返回后再次判断是否满足条件
,如果使用if
判断,不管第一个被唤醒的线程是否满足要求,就直接向下执行就会导致问题;如果采用while判断
,如果第一个条件为假,则继续轮询判断
,直到条件为真
,才跳出循环
,继续执行后续代码。
条件为真
,并发送信号
给在该条件变量
下等待的线程pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
设置条件为真为什么要加锁: 因为条件变量是利用线程之间的全局变量进行同步的机制
,要设置条件变量
,就说明要对共享的全局变量进行改变
,如果不加锁,可能会导致一些线程安全
的问题。
如果一个线程
试图对同一个互斥量加锁两次
,自身就会陷入死锁
状态。当该线程第一次去向互斥量加锁
时,由于该互斥量上并没有锁,所以可以加锁成功
,但是该线程第二次去向互斥量加锁
,由于该互斥量
上已经加过锁
了,所以会把自身挂起阻塞,直到该锁被释放
,但是自己又被挂起
了,所以不会有人去释放
的,这就造成了死锁
问题。
当一个线程去申请一个已经被持有,但是还没有释放的互斥量时,线程将会被阻塞,直到该互斥量被释放。如果该互斥量不被释放,该线程将会被一直阻塞。死锁就是,一个线程阻塞的等待一个永远不会为真的条件
。
假设程序中现在有一个互斥量
,然后一个线程
对该互斥量已经加锁
,但是在加锁和解锁的这段代码
中,如果该区域代码又试图向该互斥量申请锁
,那么就会造成自身挂起等待
,从而导致死锁
。
假设程序中使用两个互斥量
,线程A
首先锁住一个互斥量
,然后线程B
也锁住另外一个互斥量
,拥有第一个互斥量
的线程A
又去试图锁住第二个互斥量
,而拥有第二个互斥量
的线程B
试图申请锁住第一个互斥量
,这就会导致两个线程此时都在挂起堵塞
中,两个线程都在相互请求另一个线程的资源导致两个线程都无法向前运行,于是产生了死锁问题。
上边的两种产生死锁
的场景是在互斥量
的条件下,但是这造成死锁
的场景,并不局限于互斥量
,只要满足产生死锁的条件
,就会出现死锁
。针对死锁的概念
,大牛们总结出来了四条
产生死锁的必要条件
:
互斥条件与锁一样,要么能被申请,要么就只能等待。在任意时刻,某份资源只能被一个进程或线程使用。
占有和等待条件是指某个线程或进程,在占有某份资源后还可以申请其他的资源。
当某份资源被某一进程或线程占有时,不能被其他线程或进程强制性的抢占,只能被占有它的线程主动的释放。
死锁发生时,系统中一定有两个或两个以上的进程组成的一条环路,该环路中的每一个线程或进程都在等待下一个进程所占用的资源。
以上的四个条件必须同时满足
,才会可能造成死锁
。只要有一个
条件不满足,就不会造成死锁
。死锁的产生并不仅会只有使用互斥量
时会发生,只要满足以上四个条件
也可能产生死锁。在系统中,有许多只能被互斥性访问
的独占资源,如请求独占性的io设备,打印机等,在对其进行操作时,也有可能造成死锁
。
忽略死锁问题: 将死锁忽略,不注意死锁。有的死锁产生的时间并不确定
。而且死锁发生的频度
,造成问题的严重性
不同。假如对于一个死锁每隔几个月
或者每几年
出现一次,而且每次造成的问题
并不严重,那么此时,工程师可能并不会以损失可用性或性能损失的代价
去防止死锁。此种情况下就属于忽略死锁
的问题。
检测死锁并恢复:当出现死锁时,通过检测死锁
的技术,检测到出现的死锁,对于找到的死锁进行恢复
。
仔细对资源分配
,动态的避免死锁
通过破坏
引起死锁的四个必要条件之一
,以此来避免死锁
当多个线程
需要相同的一些锁
,但是按照不同的顺序加锁
,死锁
就很容易发生。如果能确保所有的线程都是按照相同的顺序
获得锁,那么死锁就不会发生
。如果一个线程(线程3)需要一些锁,那么它必须按照确定的顺序
获取锁。它只有获得了从顺序上排在前面的锁
之后,才能获取后面的锁
。例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C //此时,要锁C,必须先将释放的A锁锁住
Thread 3:
wait for A
wait for B
wait for C
缺点: 按照顺序加锁是一种有效的死锁预防机制
。但是,这种方式需要你事先知道所有可能会用到的锁
,但总有些时候是无法预知
的。
在获取锁
的时候尝试加一个获取锁的时限
,超过时限
不需要再获取锁,放弃操作(对锁的请求)。若一个线程在一定的时间里没有成功的获取到锁,则会进行回退并释放之前获取到的锁,然后等待一段时间后进行重试。在这段等待时间中其他线程有机会尝试获取相同的锁,这样就能保证在没有获取锁的时候继续执行自己的事情。
缺点: 由于存在锁的超时
,通过设置时限
并不能确定出现了死锁
,每种方法总是有缺陷
的。有时为了执行某个任务,某个线程花了很长的时间去执行任务,如果在其他线程看来,可能这个时间已经超过了等待的时限,可能出现了死锁。在大量线程去操作相同的资源
的时候,这个情况又是一个不可避免
的事情。例如,现在只有两个线程,一个线程执行的时候,超过
了等待的时间,下一个线程会尝试获取相同的锁,避免出现死锁
。但是这时候不是两个线程了,可能是几百个线程同时去执行,让事件出现的概率变大,假如线程还是等待那么长时间,但是多个线程的等待时间就有可能重叠
,因此又会出现竞争超时
,由于他们的超时发生时间正好赶在了一起,而超时等待的时间
又是一致的,那么他们下一次又会竞争
,等待,这就又出现了死锁
。
当一个线程获取锁
的时候,会在相应的数据结构
中记录下来,如果有线程请求锁
,也会在相应的结构中记录
下来。当一个线程请求失败
时,需要遍历一下这个数据结构检查是否有死锁
产生。例如:线程A请求锁住一个方法1,但是现在这个方法是线程B所有的,这时候线程A可以检查一下线程B是否已经请求了线程A当前所持有的锁,像是一个环,线程A拥有锁1,请求锁2,线程B拥有锁2,请求锁1。当遍历这个存储结构的时候,如果发现了死锁,一个可行的办法就是释放所有的锁,回退,并且等待一段时间后再次尝试。
缺点: 这个这个方法和上面的超时重试
的策略是一样的。但是在大量线程
的时候问题还是会出现和设置加锁时限
相同的问题。每次线程之间发生竞争
。 还有一种解决方法是设置线程优先级
,这样其中几个线程回退
,其余的线程继续保持着他们获取的锁,也可以尝试随机设置优先级
,这样保证线程的执行
。