一:mutex(互斥量)
多个线程同时访问共享数据时可能会冲突,比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:
1:从内存读取变量值到寄存器
2:寄存器的值加1
3:将寄存器的值写回内存
如:我们创建两个线程,各自把全局count增加5000,我们希望最后的count应该是10000次,可是每次运行的结果都不一样.
如:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/types.h>
static int count =0;
void*pthread_run(void*arg)
{
int tmp = 0;
int i = 0;
while(i<5000)
{
++i;
tmp = count;
printf("thread is %u,count is %d\n",pthread_self(),count);
count =tmp+1;
}
return NULL;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,pthread_run,NULL);
pthread_create(&tid2,NULL,pthread_run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("count is %d\n",count);
return 0;
}
我们看下结果:
第一次:
第二次:
那么是如何造成这个问题的呢?
这就是常说的在多线程的程序中,访问冲突的问题.解决的办法是引入互斥锁(Mutex,Mutex Exclusive Lock),获得锁的线程可以完成”读-修改-写”操作,然后释放锁给其他线程,没有获得锁的线程只能等待而不能访问共享数据,这样”读-修改-写”三部的操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其他处理器上并行做这个操作.
Mutex用pthead_mutex_t类型的变量表示,可以这样表示初始化和销毁
返回值:成功返回0,失败返回错误号
我们可以看到pthead_mutex_init函数对Mutex做初始化,设定Mutex的属性,如果attr为NULL则表示缺省属性.当然我们也可以pthead_mutex_destory销毁,如果Mutes变量是静态分配的(全局变量或者static变量),也可以用宏定义PTHREAD_MUTES_INITIALIZER来初始化,相当于用pthread_mutex_init初始化,并且attr参数为NULL,
二:Mutex的加锁和解锁:
三个函数:
pthread_mutex_lock:以原子操作一个互斥锁加锁,如果已经目标已经被加锁,则pthread_mutex_lock被阻塞,直到互斥锁占有者给它解锁.
pthread_mutex_trylock:和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否加锁,当目标被倍加锁时,pthread_mutex_teylock进行加锁操作,否则将返回EBUSY错误码
pthread_mutex_unlock:以原子的操作给一个互斥锁进行解锁.
返回值:成功返回0,失败返回错误号;
返回值:成功返回0,失败返回错误号;
通俗的说一个线程可以调用pthread_mutex_lock获得Mutex(互斥量),如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex(互斥量),则当前进程需要挂起等待,直到另外一个线程调用那个pthread_mutex_unlock释放Mutex,当前进程被唤醒,才能获得该Mutex并继续执
如果一个线程即想获得锁,又不想调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会是线程挂起等待.
//验证
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/types.h>
static int count =0;
pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;//表明初始化
void*pthread_run(void*arg)
{
int tmp = 0;
int i = 0;
while(i<5000)
{
++i;
pthread_mutex_lock(&mutex_lock);//加锁
tmp = count;
printf("thread is %u,count is %d\n",pthread_self(),count);
count =tmp+1;
pthread_mutex_unlock(&mutex_lock);//释放锁
}
return NULL;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,pthread_run,NULL);
pthread_create(&tid2,NULL,pthread_run,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("count is %d\n",count);
return 0;
}
通过上面的实例我们知道互斥量本质上来说就是一把锁,提供对共享资源的保护访问.
看到这个结果可能会感到奇怪,Mutex的两个基本操作lock和unlock是如何实现的?前面我们知道多个线程访问时会出现冲突的问题,当引入互斥锁后,当一个线程加锁后其他线程只能挂起等待,只与等这个线程获得解锁后,唤醒等待的其他线程.这样我们就不难理解第二次这两个全局的Count进行增加5000次,结果是10000了.
我们可以用伪代码来表示下加锁和解锁:
lock:
if(mutex > 0 )
{
mutex =0;
return 0 ;
}
else:
lock;挂起等待
unlock:
mutex = 1;
唤醒等待Mutex的线程;
return 0;
当然了unlock操作中唤醒等待线程可以有不同的实现,可以唤醒一个等待线程,也可以唤醒所有等待该Mutex的线程,然后让这些被唤醒的这些线程去竞争获得这个Mutex,竞争失败的线程继续挂起等待
“挂起等待”和”唤醒等待线程”
每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先把自己假如等待队列中,然后置线程状态为睡眠.然后调度器函数切换到别的线程,只需从等待队列中取一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行是就有可能切换到被唤醒的线程,
三:死锁
死锁现象:
如果同一个线程先后矿磁调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着,该线程又被挂起而美欧机会释放锁,因此就永远挂起等待状态了,这叫死锁.
另外一种典型的死锁:
线程A获得锁1,线程B获得锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和线程B都永远处于挂起状态了.
产生死锁的四个必要条件:
1:互斥条件:一个资源每次只能被一个进程(线程)使用;
2:占有和等待条件:已经分配给某个资源的线程可以请求新的资源
3:不可抢占条件:已经分配给一个进程的资源不能强行地被抢占,它只能占有它的进程显示的释放
4:循环等待条件:系统中一定有两个或者两个以上的进程组成一条环路,该环路中的每一个进程都在等待着下一个进程所占有的资源。
对线程之间形成一种头尾相接的循环等待资源关系
预防死锁:
预防死锁的根本办法就是要使死锁产生的4个必要条件之一不存在.
1:破坏互斥条件:
破坏互斥条件即允许多个进程访问资源,由于多数资源的必须互斥访问,因此通过破坏互斥条件多数情况下是行不通的
2:破坏占有和等待条件
采用资源静态分配法,就是线程运行前,一次性的分配它运行所需要的全部资源
若系统有足够的资源分配给某一个线程,则一次性第将其所需要的资源分配给该线程,这样运行期间就不会提出任何资源请求,从而是等待条件不成立.如果分配时有一种资源要求不能满足,则线程需要其他资源也先不分配给线程,从而避免进程在等待期间占用任何资源,破坏占用条件,从而避免死锁的发生.
3:破坏不剥夺条件
使一个已保持了某些资源的进程,由于新的资源要求目前得不到满足,它必须先暂时释放以保持的所有资源,然后去等待,以后再向系统申请,这样也能防止死锁
4:破坏循环等待条件:
申请新锁的时候,先把自己的锁释放,在申请新锁,就不会陷入循环等待中没这样就可以解除死锁.
解除死锁:
死锁的解除的实值让释放资源的线程能够继续运行
1:剥夺资源:
使用挂起机制挂起一些线程,剥夺它们占有的资源给死锁线程,以解除死锁,等到条件满足后,再唤醒被挂起的线程
2:撤销进程
可以直接撤销代价最小的进程达到解除死锁的目的,直到有足够的资源可用解除死锁.