当前位置:首页 > 公众号精选 > 码农的荒岛求生

关于死锁你能搜到很多千篇一律的资料,今天我们来讲点不一样的,那就是从内核视角看线程是如何一步步死锁的。

我们知道线程是由内核调度器决定是否运行的,调度器本质是通过线程状态来判定运行哪个线程,进程/线程状态图想必大家都了解:

只有位于就绪态(ready)的线程才有资格被调度器调度。

来看一个简单的死锁示例:

// 线程 1:先锁 A,再锁 B
void* thread1_func(void* arg) {
    pthread_mutex_lock(&mutex_A); // 锁 A
    pthread_mutex_lock(&mutex_B); // 锁 B
    // 省略临界区操作...
    pthread_mutex_unlock(&mutex_B);
    pthread_mutex_unlock(&mutex_A);
    returnNULL;
}

// 线程 2:先锁 B,再锁 A
void* thread2_func(void* arg) {
    pthread_mutex_lock(&mutex_B); // 锁 B
    pthread_mutex_lock(&mutex_A); // 锁 A
    // 省略临界区操作...
    pthread_mutex_unlock(&mutex_A);
    pthread_mutex_unlock(&mutex_B);
    returnNULL;
}

假设现在一个单核系统中只有两个线程,线程A和线程B,这两个线程都刚被创建出来,此时的内核状态是:

现在调度器决定首先调度线程A,线程A来到thread1_func拿到锁A后系统产生中断,此时内核把线程A置于就绪态后处理中断,中断处理完毕后决定调度线程B,线程B开始运行,此时的内核状态:

线程B来到thread2_func后先拿锁B,接着拿锁A时发现锁A已被占用,因此线程B来到阻塞态,此时的内核状态:

接着调度器决定运行处于就绪态的线程A,线程A继续运行去拿锁B,但发现锁已被占用,因此线程A也来到阻塞态,此时的内核状态:

现在所有线程来到了阻塞态,注意看此时的就绪队列是空的,无就绪线程可供调度,内核调度器认为此时没有任何一个线程需要分配CPU,此时CPU来到空闲状态。

死锁的本质在于调度器只会去调度位于就绪态的线程,而位于阻塞状态的线程因为资源依赖关系无法改变线程状态,从而形成僵局,但此时在内核看来系统一切正常,如果有个新的线程还可以继续运行。

内核对位于阻塞态的线程A和线程B是视而不见的,因为内核调度器仅关注线程状态,不跟踪资源依赖关系。

你可能会问既然调度器知道线程的状态,为什么内核不能主动检测死锁?

因为调度器仅跟踪线程的执行状态(如运行、就绪、阻塞),不维护资源(如锁、信号量)的分配关系,死锁涉及资源分配以及跟踪策略,这部分通常由用户态应用程序自己处理。而且如果让内核去检测所有线程的资源依赖的话开销过高。

数据库系统是最典型的自带死锁检测机制的系统,由于数据库需要处理高并发事务和共享资源(如数据行、表、索引),死锁检测是事务管理的关键功能,这通常基于等待图(Wait-for Graph)算法,周期性地检查事务间的等待关系,发现循环等待则判定为死锁。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭