Linux系统编程系列之死锁

一、什么是死锁

        死锁是指在并发编程中,两个或多个进程或线程在竞争使用资源时,由于彼此持有对方需要的资源而互相等待,导致程序无法继续执行的情况。

二、发生死锁的情况

        1、互斥资源循环等待

        多个线程或进程之间形成了一个等待的循环链,每个线程或进程都在等待下一个资源被释放

        2、持有和等待

        一个线程或进程在持有资源的同时等待其他资源,可能导致资源无法被有效利用

        3、不可抢占资源

        某些资源无法被抢占,只能等待持有该资源的线程或进程主动释放

        4、循环等待资源

        多个线程或进程之间形成一种循环等待资源的情况,形成一个环路

        举例:当某条线程在对锁资源进行上锁后,在解锁前被意外取消!这样锁资源永远被该线程占用,且没有释放。

        解决方法:

        1、可以使用压入线程取消处理函数到栈中的方法,来预防未来可能会收到取消请求,在取消处理函数中对锁资源进行释放

        2、可以使用线程的取消状态进行设置,当前线程在获取到锁资源后,暂时不接受取消请求,直到把锁资源释放后,再重新开启取消请求的响应

三、如何预防或者解决死锁

        1、破坏循环等待条件

        通过强制线程或进程按照特定的顺序获取锁资源,避免形成循环依赖的等待条件

        2、一次性申请所有需要的资源

        线程或进程在执行时一次性申请所有需要的资源,避免持有部分资源等待其他资源,降低发生死锁的风险

        3、资源剥夺和回滚

        当线程或进程请求资源超时或无法满足时,可以选择剥夺已经获得的资源,然后回滚,释放资源,从而避免可能的死锁(特别是安装某些软件时,经常会回滚,删都删不掉,删不干净!)

        4、设定资源的有限等待时间

        线程或进程在等待资源时设置有限的等待时间,如果超过预定的时间仍无法获取资源,则放弃当前请求或采取其他补救措施

        5、合理规划资源使用

        通过良好的资源管理和分配策略,避免资源竞争,减少死锁的发生概率

        6、死锁检测和恢复

        通过死锁检测算法检测死锁的发生,一旦检测到死锁,采取相应的解决措施,如资源回收、终止进程或线程,从而恢复系统的正常运行

        其他方法:直接取消所有程序,不能取消就关机重启,不能重启就拔电(非常不推荐,最后一招必杀技,治标不治本!)

四、相关函数API接口

        1、取消某个线程

// 给指定线程发送一个取消请求
int pthread_cancel(pthread_t thread);

// 接口说明
        返回值:成功返回0,失败返回错误码
        参数thread:要取消的线程TID号

         2、设置线程的取消状态

// 设置线程的取消状态
int pthread_setcancelstate(int state, int *oldstate);

// 接口说明
        返回值:成功返回0,失败返回错误码
        参数state:新的取消状态,有两种
            (1) PTHREAD_CANCEL_ENABLE    使能取消请求(默认)
            (2) PTHREAD_CANCEL_DISABLE    关闭取消请求
        参数oldstate:旧的取消状态


// 设置线程的取消类型
int pthread_setcanceltype(int type, int *oldtype);

// 接口说明
        返回值:成功返回0,失败返回错误码
        参数type:新的取消类型,有两种
            (1)PTHREAD_CANCEL_DEFERRED    延时响应(默认)
            (2) PTHREAD_CANCEL_ASYNCHRONOUS    立即响应

        参数oldtype:旧的取消类型

        3、压栈和弹栈线程的取消处理

// 压栈线程的取消处理
void pthread_cleanup_push(void (*routine)(void *),
                                 void *arg);

// 接口说明
        返回值:没有返回值
        参数routine:线程的取消处理例程,就是一个回调函数
        参数arg:线程取消处理例程的参数


// 弹栈线程的取消处理
void pthread_cleanup_pop(int execute);

// 接口说明
        返回值:没有返回值
        参数execute:一个选择参数,分两种情况
            (1)0:弹栈线程的取消处理例程,但不执行该例程
            (2)非0:弹栈线程的取消处理例程,并执行该例程

注意点:
(1)这两个函数必须配套使用,而且必须出现在同一层代码块中
(2)可以为线程的取消请求压入多个处理例程,然后以栈的形式保留起来,然后以弹栈的形式先进后出执行
       

五、案例

        1、使用线程取消函数例程完成某个线程被意外取消后,解决死锁的演示

// 读写锁的案例

#include 
#include 
#include 
#include 
#include 

int data = 100; // 共享变量

pthread_rwlock_t data_rwlock; // 定义互斥锁变量
pthread_once_t data_rwlock_once_init;    // 函数单例初始化变量
pthread_once_t data_rwlock_once_destroy;    // 函数单例销毁变量

// 初始化互斥锁data_rwlock
void data_rwlock_init(void)
{
    pthread_rwlockattr_t data_rwlock_attr;
    // 设置锁属性为写锁优先
    // 1、初始化读写锁属性
    pthread_rwlockattr_init(&data_rwlock_attr);

    // 2、设置读写锁属性为写锁优先
    pthread_rwlockattr_setkind_np(&data_rwlock_attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);

    // 3、动态初始化锁资源,此时读写锁是写锁优先的
    pthread_rwlock_init(&data_rwlock, NULL);

    // 4、销毁读写锁属性
    pthread_rwlockattr_destroy(&data_rwlock_attr);
}

// 销毁互斥锁data_rwlock
void data_rwlock_destroy(void)
{
    pthread_rwlock_destroy(&data_rwlock);
}

// 线程取消的处理函数
void pthread_cancel_handler1(void *arg)
{
    printf("call pthread_cancel_handler1\n");

    printf("pthread is cancel\n");
}


// 线程取消的处理函数
void pthread_cancel_handler2(void *arg)
{
    printf("call pthread_cancel_handler2\n");

    // 释放锁资源
    pthread_rwlock_unlock(&data_rwlock);
    
    printf("unlock\n");
}


// 线程1的例程函数,用来接收数据
void *recv_routine(void *arg)
{
    printf("I am recv_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_init, data_rwlock_init);

    while(1)
    {
        // 加上读锁
        pthread_rwlock_rdlock(&data_rwlock);    
        if(data % 2)
        {
            printf("%d 是奇数\n", data);
        }
        else
        {
            printf("%d 是偶数\n", data);
        }
        // 解锁
        pthread_rwlock_unlock(&data_rwlock);

        sleep(1);    // 放慢速度
    }

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
}

// 线程2的例程函数,用来发送数据
void *send_routine(void *arg)
{
    printf("I am send_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 设置线程的取消状态为可以取消
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    // 设置线程的取消类型为立即响应
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    // 压栈线程取消处理的函数例程,压入两个函数,以出栈的形式执行
    pthread_cleanup_push(pthread_cancel_handler2, NULL);
    pthread_cleanup_push(pthread_cancel_handler1, NULL);

    // 不能把弹栈函数放在这里,这样会直接弹栈
    // pthread_cleanup_pop(1);
    // pthread_cleanup_pop(1);
    
    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_init, data_rwlock_init);
    
    sleep(3);   // 保证让线程1先执行
    while(1)
    {
        // 加上写锁
        pthread_rwlock_wrlock(&data_rwlock);

        data++;

        // 线程自己取消自己,简称自杀
        pthread_cancel(pthread_self());
    
        // 解锁
        pthread_rwlock_unlock(&data_rwlock);
    }

    // 弹栈线程取消处理,执行栈中的函数
    // 必须放在程序末尾,不能放在while前面,否则会直接执行
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;

    // 创建线程1,用来接收数据
    errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create recv_routine success, tid = %ld\n", tid1);
    }
    else
    {
        perror("pthread create recv_routine fail\n");
    }

    
    // 创建线程2,用来发送数据,线程拥有分离属性
    errno = pthread_create(&tid2, NULL, send_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create send_routine success, tid = %ld\n", tid2);
    }
    else
    {
        perror("pthread create send_routine fail\n");
    }

    // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    // 或者加上while(1)等让主函数不退出
    pthread_exit(0);
    
    return 0;
}

Linux系统编程系列之死锁_第1张图片

        2、使用在临界区屏蔽线程取消的方法完成对某个线程被意外取消时,解决死锁的演示

// 读写锁的案例

#include 
#include 
#include 
#include 
#include 

int data = 100; // 共享变量

pthread_rwlock_t data_rwlock; // 定义互斥锁变量
pthread_once_t data_rwlock_once_init;    // 函数单例初始化变量
pthread_once_t data_rwlock_once_destroy;    // 函数单例销毁变量

// 初始化互斥锁data_rwlock
void data_rwlock_init(void)
{
    pthread_rwlockattr_t data_rwlock_attr;
    // 设置锁属性为写锁优先
    // 1、初始化读写锁属性
    pthread_rwlockattr_init(&data_rwlock_attr);

    // 2、设置读写锁属性为写锁优先
    pthread_rwlockattr_setkind_np(&data_rwlock_attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);

    // 3、动态初始化锁资源,此时读写锁是写锁优先的
    pthread_rwlock_init(&data_rwlock, NULL);

    // 4、销毁读写锁属性
    pthread_rwlockattr_destroy(&data_rwlock_attr);
}

// 销毁互斥锁data_rwlock
void data_rwlock_destroy(void)
{
    pthread_rwlock_destroy(&data_rwlock);
}

// 线程1的例程函数,用来接收数据
void *recv_routine(void *arg)
{
    printf("I am recv_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_init, data_rwlock_init);

    while(1)
    {
        // 加上读锁
        pthread_rwlock_rdlock(&data_rwlock);    
        if(data % 2)
        {
            printf("%d 是奇数\n", data);
        }
        else
        {
            printf("%d 是偶数\n", data);
        }
        // 解锁
        pthread_rwlock_unlock(&data_rwlock);

        sleep(1);   // 放慢速度
    }

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
}

// 线程2的例程函数,用来发送数据
void *send_routine(void *arg)
{
    printf("I am send_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 设置线程的取消类型为立即响应
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_init, data_rwlock_init);
    
    // sleep(3);   // 保证让线程1先执行
    while(1)
    {
        // 进入临界区前,先屏蔽可以被取消
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
        // 加上写锁
        pthread_rwlock_wrlock(&data_rwlock);

        data++;

        // 解锁
        pthread_rwlock_unlock(&data_rwlock);

        // 出临界区后,接收取消
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

        sleep(1);   // 放慢速度
    }

    // 函数单例,本程序只会执行data_rwlock_init()一次
    pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;

    // 创建线程1,用来接收数据
    errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create recv_routine success, tid = %ld\n", tid1);
    }
    else
    {
        perror("pthread create recv_routine fail\n");
    }

    // 创建线程2,用来发送数据,线程拥有分离属性
    errno = pthread_create(&tid2, NULL, send_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create send_routine success, tid = %ld\n", tid2);
    }
    else
    {
        perror("pthread create send_routine fail\n");
    }

    sleep(5);
    printf("call pthread cancel to cancel pthread2\n");
    pthread_cancel(tid2);   // 5秒后取消线程2

    // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    // 或者加上while(1)等让主函数不退出
    pthread_exit(0);
    
    return 0;
}

Linux系统编程系列之死锁_第2张图片

六、总结

        死锁一般是无法避免的,就像误差一样,但是可以通过一定的方法,来预防、避免和解决发生的死锁。当线程被意外取消时,解决方法有设置一个线程取消例程函数或者在临界区屏蔽线程取消的方法,或者具体看看上面的讲解或者结合案例加深理解。

你可能感兴趣的:(C语言程序设计,Linux,c语言,linux)