1.互斥锁类型
1.普通锁 (PTHREAD_MUTEX_NORMAL)
互斥锁默认类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按照优先级获得它,这种锁类型保证了资源分配的公平性。一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;
对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
2.检错锁(PTHREAD_MUTEX_ERRORCHECK)
一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK;
对一个已经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM;
3.嵌套锁(PTHREAD_MUTEX_RECURSIVE)
该锁允许一个线程在释放锁之前多次对它加锁而不发生死锁;
其他线程要获得这个锁,则当前锁的拥有者必须执行多次解锁操作;
对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM
4.默认锁(PTHREAD_MUTEX_ DEFAULT)
一个线程如果对一个已经加锁的默认锁再次加锁,或者虽一个已经被其他线程加锁的默认锁解锁,或者对一个解锁的默认锁解锁,将导致不可预期的后果;
这种锁实现的时候可能被映射成上述三种锁之一;
2.死锁举例
1.不同线程申请普通锁,将按照顺序依次申请锁并加锁
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void * thread_func_one(void *arg)
{
int i;
for(i=0;i<10;i++){
pthread_mutex_lock( &mutex1);
count++;
sleep(1);
pthread_mutex_unlock(&mutex1);
printf("thread one count value is %d/n",count);
}
return NULL;
}
void * thread_func_two(void *arg)
{
int i;
for(i=0;i<10;i++){
pthread_mutex_lock( &mutex1);
//pthread_mutex_lock( &mutex1);
count++;
sleep(1);
//pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex1);
printf("thread two count value is %d/n",count);
}
return NULL;
}
int main ( int argc, char **argv)
{
pthread_t thread_one, thread_two;
if( 0!=pthread_create( &thread_one, NULL, thread_func_one,NULL)){
printf("pthread create failed!/n");
return -1;
}
if( 0!=pthread_create( &thread_one, NULL, thread_func_two,NULL)){
printf("pthread create failed!/n");
return -1;
}
pthread_join(thread_one, NULL);
pthread_join(thread_two,NULL);
return 0;
}
2.一个线程对一个普通锁两次加锁将导致死锁
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void * thread_func_one(void *arg)
{
int i;
for(i=0;i<10;i++){
pthread_mutex_lock( &mutex1);
pthread_mutex_lock( &mutex1);//锁两次
count++;
sleep(1);
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex1);
printf("thread one count value is %d/n",count);
}
return NULL;
}
void * thread_func_two(void *arg)
{
int i;
for(i=0;i<10;i++){
pthread_mutex_lock( &mutex1);
//pthread_mutex_lock( &mutex1);
count++;
sleep(1);
//pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex1);
printf("thread two count value is %d/n",count);
}
return NULL;
}
int main ( int argc, char **argv)
{
pthread_t thread_one, thread_two;
if( 0!=pthread_create( &thread_one, NULL, thread_func_one,NULL)){
printf("pthread create failed!/n");
return -1;
}
if( 0!=pthread_create( &thread_one, NULL, thread_func_two,NULL)){
printf("pthread create failed!/n");
return -1;
}
pthread_join(thread_one, NULL);
pthread_join(thread_two,NULL);
return 0;
}
3.按不同顺序访问互斥锁导致死锁
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int a = 0;
int b = 0;
pthread_mutex_t mutexa;
pthread_mutex_t mutexb;
void* another(void* arg){
pthread_mutex_lock(&mutexb);
printf("in child thread, got mutex b, waitiing for mutex a\n");
sleep(5);
++b;
pthread_mutex_lock(&mutexa);
b += a++;
pthread_mutex_unlock(&mutexa);
pthread_mutex_unlock(&mutexb);
pthread_exit(NULL);
}
int main ( int argc, char **argv)
{
pthread_t id;
pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create( &id, NULL, another,NULL);
pthread_mutex_lock(&mutexa);
printf("in parent thread, got mutex a, waiting for b\n");
sleep(5);
++a;
pthread_mutex_lock(&mutexb);
a += b++;
pthread_mutex_unlock(&mutexb);
pthread_mutex_unlock(&mutexa);
pthread_join(id, NULL);
pthread_mutex_destroy(&mutexa);
pthread_mutex_destroy(&mutexb);
return 0;
}
分析:
主线程试图先占有互斥锁mutex_a,且又申请互斥锁mutex_b,并在两个互斥锁的保护下,操作变量a和b,最后才一起释放两个互斥锁;
与此同时,子线程按照相反的顺序来申请互斥锁mutex_a和mutex_b,并在两个锁的保护下操作变量a和b。
我们用sleep(5)为了模拟连续两次调用pthread_mutex_lock之间的时间差,以确保代码中的两个线程各自先占有一个互斥锁(主线程占有mutex_a,子线程占有mutex_b),然后等待另外一个互斥锁(主线程等待mutex_b,子线程等到mutex_a)
两个线程将僵持住,谁都不能继续往下执行,从而形成死锁;
如果代码中不加sleep函数,也许这段代码能够运行成功,但是会为程序留下一个潜在哦BUG.
4.在多线程程序中调用fork导致死锁
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <wait.h>
pthread_mutex_t mutex;
void* another(void* arg){
printf("in child thread, lock the mutex\n");
pthread_mutex_lock(&mutex);
sleep(5);
pthread_mutex_unlock(&mutex);;
}
int main ( int argc, char **argv)
{
pthread_t id;
pthread_mutex_init(&mutex, NULL);
pthread_create( &id, NULL, another,NULL);
/*父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex*/
sleep(1);
int pid = fork();
if( pid < 0 ){
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 1;
}
else if( pid == 0 ){
printf("i am in the child,want to get the lock\n");
/*子进程从父进程继承了互斥锁的状态,该互斥锁处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,因此,下面这句加锁操作会一直阻塞,因为fork出来的子进程拷贝一份主进程的内存,但是主进程和子进程是两份不同的内存,即使主进程中的子线程释放了锁,也不会对子进程中的锁造成任何影响*/
pthread_mutex_lock( &mutex );
printf( "i can not run t here, opp..." );
pthread_mutex_unlock( &mutex );
exit(0);
}
else{
wait(NULL);
}
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 0;
}
分析:
子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制。
子进程中将自动继承父进程中的互斥锁的状态,也就是说,父进程中已经被加锁的互斥锁在子进程中也是锁住的,这将引发下面问题:
子进程可能不清楚从父进程继承而来的互斥锁的具体状态(是加锁状态还是解锁状态);这个互斥锁已经加锁了,但不是由调用fork函数的那个线程锁住的,而是由其他线程锁住的。由于主进程和子进程有两份不同的内存,即使主进程中的子线程释放掉锁,也不会影响子进程中的锁的状态,这种情况就是上面代码情况,导致死锁;
针对上面代码情况死锁,pthread提供了一个专门函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态:int pthread_arfork(void (*prepare)(void), void (*parent)(void),void (*child)(void)) ;
上面该函数将建立3个fork句柄来帮助我们清理互斥锁的状态。prepare句柄将在fork调用创建出子进程之前被执行,它用来锁住所有父进程中的互斥锁;parent句柄这是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行,它的作用:释放所有在prepare句柄中被锁住的互斥锁;child句柄是fork返回之前,在子进程中被执行,和parent句柄一样,child句柄也是用于释放所有在prepare句柄中被锁住的互斥锁。该函数成功返回0,失败返回错误代码。
通过将pthread_atfork代替fork避免死锁
void prepare(){
pthread_mutex_lock( &mutex );
}
void infork(){
pthread_mutex_unlock( &mutex );
}
pthread_atfork( prepare, infork, infork );