线程同步---互斥锁和用锁不当造成的死锁现象

一、互斥锁
1.什么是互斥锁/互斥量?
  • 互斥锁是线程同步中的一种机制,可以锁定一个代码块,也即该代码块(涉及到某块共享资源)被这个锁保护
  • 假如互斥锁被线程A锁定,那么在线程A对互斥锁解锁之前,其余的所有线程都不能获取该锁,并被阻塞在上锁的位置
  • 二元信号量和互斥锁十分类似,但是信号量可以被不同的线程占有释放
  • 互斥锁被一个线程锁定后,只能被同一个线程解锁之后才能被其他线程占用锁定
  • 一般情况下,每一个共享资源对应一个互斥锁锁的个数线程的个数无关

2.互斥锁的API函数
// Linux中的互斥锁类型pthread_mutex_t
pthread_mutex_t mutex
    
// 初始化所资源
// mutex:互斥锁的变量地址
// attr:互斥锁的属性,一般默认属性既可,NULL
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t *restrict attr);

// 释放互斥锁的资源
int pthread_mutex_destroy(pthread_mutex_t* mutex);

// 加锁(加锁不成功时会阻塞线程)
int pthread_mutex_lock(pthread_mutex_t* mutex);

// 尝试加锁(枷锁不成功时直接返回失败的错误信号)
int pthread_mutex_trylock(pthread_mutex_t* mutex);

// 解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);

加锁之后的多线程示例代码

#include 
#include 
#include 

int number = 0;
pthread_mutex_t mutex;

void* counterA(void* arg) {
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex);  // 加锁
        int cur = number;
        cur++;
        usleep(4);
        number = cur;
        pthread_mutex_unlock(&mutex); // 解锁
        printf("线程A---线程A地址:%ld---number:%d\n", pthread_self(), number);
    }
    return NULL;
}

void* counterB(void* arg) {
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex); // 加锁
        int cur = number;
        cur++;
        usleep(4);
        number = cur;
        pthread_mutex_unlock(&mutex); // 解锁
        printf("线程B---线程B地址:%ld---number:%d\n", pthread_self(), number);
    }
    return NULL;
}


int main()
{
    pthread_t t1;
    pthread_t t2;

    // 初始化锁
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, counterA, NULL);
    pthread_create(&t2, NULL, counterB, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}
总结
  • 所谓加锁,其实是附加了一个条件,也就是在获得锁的条件下才能运行锁住的代码
  • 锁应该是多个线程的共享资源,这样才能起到避免竞争的作用
二、死锁
1.什么是死锁?
  • 死锁:是因为线程在加锁时使用不当,造成的所有的线程都被阻塞的情况,并且线程的阻塞无法被打开,属于无解状态,就是死锁
2.死锁场景

①加锁之后忘记解锁

void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        // 忘记解锁
    }
}void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        if(xxx)
        {
            // 函数退出, 没有解锁(解锁函数无法被执行了)
            return ;
        }
        
        pthread_mutex_lock(&mutex);
    }
}
// 线程A加锁后会阻塞所有除了线程A之外的线程,其他线程都在等待线程A解锁
// 忘记解锁后,线程A又一次循环到加锁位置,无法加锁吗,因为还没有解锁
// 此时的状态:没有任何一个线程可以解锁,所以的线程都被阻塞了

②重复加锁,造成死锁

void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A线程阻塞
        pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}// 隐藏的比较深的情况
void funcA()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

void funcB()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        funcA();		// 重复加锁
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

// funcB()加锁之后,调用funcA(),又加了一遍锁,造成了重复加锁
// 重复加锁造成死锁的原因:
// 加锁之后,阻塞了其余的所有线程;因此必须先解锁;
// 当没有解锁,又一次加锁时,因为锁处于锁定状态,所以仅剩的线程也被阻塞了,最终造成了死锁状态

③程序中有多个共享资源,因此有很多锁,随意加锁,导致互相被阻塞(互锁

场景描述:
  step1. 有两个共享资源:X, Y,X对应锁Lx, Y对应锁Ly
     - 线程A当前正在访问资源X, 故加了锁Lx
     - 线程B当前正在访问资源Y, 故加了锁Ly
  step2. 线程A紧接着又要访问资源Y, 线程B紧接着要访问资源X,目前的状态如下:
     - 线程A要访问Y资源但是Y资源正在被线程B占用,故被锁Ly阻塞了, 线程“卡住”,于是也没有机会打开Lx锁
     - 线程B要访问X资源但是X资源正在被线程A占用,故被锁Lx阻塞了, 线程“卡住”,于是也没有机会打开Ly锁

这种情况就是两个线程分别占用了一个资源,同时自己占用的资源又是对方当前所需要的资源,双方因为所需的资源被锁而阻塞,因此也就没有机会释放自己所占用的资源,所以对方也就无法解除阻塞,双方都在等待对方解锁使自己解除阻塞,但是不解除阻塞就无法解锁,这就导致两个线程互相锁定。

3.死锁现象总结
  • 线程A、B分别因为使用资源X、Y锁定了资源X和Y
  • 线程A锁定了资源X,又需要资源Y,但是资源Y被线程B锁定了,所以线程A阻塞,因为获取不到资源Y,所以线程A无法退出,也就无法释放资源X
  • 线程B锁定了资源Y,又需要资源X,但是资源X被线程A锁定了,所以线程B阻塞,因为获取不到资源X,所以线程B无法退出,也就无法释放资源Y
  • 最终造成了相互锁定的死锁局面

参考文献:

①《程序员的自我修养》
② https://subingwen.cn/linux/thread-sync/

你可能感兴趣的:(多线程编程,编译,链接,库,互斥锁,死锁,信号量,多线程)