死锁 详解

目录

前言

一、什么是死锁

二、死锁产生的四个必要条件

[1. 互斥条件](#1. 互斥条件)

[2. 请求与保持条件](#2. 请求与保持条件)

[3. 不剥夺条件](#3. 不剥夺条件)

[4. 循环等待条件](#4. 循环等待条件)

三、经典死锁示例

四、如何避免死锁?

例方法1:破坏循环等待(锁顺序)

例方法2:破坏持有并等待(一次性申请)

例方法3:破坏不可剥夺(超时)


前言

死锁是指多个线程因争夺资源而互相等待的阻塞现象。

其产生的四个必要条件是:互斥条件、请求与保持条件、不剥夺条件和循环等待条件。

本文通过示例代码展示了两个线程因获取锁的顺序不同而导致的死锁情况。

解决方法包括固定锁获取顺序、使用trylock或超时锁等。

避免死锁的关键在于打破四个必要条件之一,如一次性分配所有资源或按固定顺序获取锁。


一、什么是死锁

死锁指两个或多个线程 在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干预,它们都将无法继续执行。

举个生活中的例子:

线程A拿着锁1,等待锁2
线程B拿着锁2,等待锁1
两者互相等待,谁也无法继续执行

二、死锁产生的四个必要条件

1. 互斥条件

资源在同一时刻只能被一个线程占用。

2. 请求与保持条件

线程已经持有至少一个资源,同时又请求其他资源,且不释放已持有的资源。

3. 不剥夺条件

线程已获得的资源在未使用完之前,不能被其他线程强行剥夺。

4. 循环等待条件

多个线程之间形成一种头尾相接的循环等待资源关系。

三、经典死锁示例

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 定义两个互斥锁,并初始化为默认属性
// PTHREAD_MUTEX_INITIALIZER 是静态初始化宏,相当于调用 pthread_mutex_init 使用默认属性
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

/**
 * 线程1 的执行函数
 * 它会先锁定 mutex1,然后尝试锁定 mutex2,但中间 sleep 1 秒,
 * 这使得线程2 有机会先锁定 mutex2,从而形成死锁。
 */
void *thread1_func(void *arg) {
    printf("线程1: 尝试获取互斥锁1...\n");
    pthread_mutex_lock(&mutex1);          // 获取锁1,成功则继续,否则阻塞
    printf("线程1: 获得了互斥锁1\n");
    
    // 模拟一些操作,增加死锁概率
    // 休眠1秒,让线程2 有机会获得锁2
    sleep(1);
    
    printf("线程1: 尝试获取互斥锁2...\n");
    // 尝试获取锁2,此时锁2可能已被线程2 持有,因此线程1 阻塞,等待线程2 释放锁2
    pthread_mutex_lock(&mutex2);  // 等待线程2释放锁2
    printf("线程1: 获得了互斥锁2\n");
    
    // 执行操作(临界区)
    // 这里可以放共享资源的访问代码
    
    // 释放锁,顺序与加锁相反
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    
    return NULL;
}

/**
 * 线程2 的执行函数
 * 它会先锁定 mutex2,然后尝试锁定 mutex1,与线程1 的加锁顺序相反,
 * 从而造成循环等待的死锁局面。
 */
void *thread2_func(void *arg) {
    printf("线程2: 尝试获取互斥锁2...\n");
    pthread_mutex_lock(&mutex2);          // 获取锁2
    printf("线程2: 获得了互斥锁2\n");
    
    sleep(1);  // 确保线程1 已经获得锁1
    
    printf("线程2: 尝试获取互斥锁1...\n");
    // 尝试获取锁1,此时锁1被线程1 持有,线程2 阻塞,等待线程1 释放锁1
    pthread_mutex_lock(&mutex1);  // 等待线程1释放锁1
    printf("线程2: 获得了互斥锁1\n");
    
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    
    // 创建两个线程,分别执行 thread1_func 和 thread2_func
    pthread_create(&tid1, NULL, thread1_func, NULL);
    pthread_create(&tid2, NULL, thread2_func, NULL);
    
    // 等待两个线程结束,但由于死锁,它们将永远无法结束,所以程序会卡在这里
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

死锁产生的原因分析:

  1. 线程1 持有 mutex1,等待 mutex2。
  2. 线程2 持有 mutex2,等待 mutex1。
  3. 两个线程互相等待对方释放自己需要的锁,且不会主动释放已持有的锁,
    形成了循环等待条件,满足死锁的四个必要条件(互斥、持有并等待、不可剥夺、循环等待)。

解决方法示例:

  • 固定锁的获取顺序(例如总是先锁 mutex1 再锁 mutex2)。
  • 使用 pthread_mutex_trylock 尝试加锁,若失败则释放已持有的锁并重试。
  • 使用超时锁(如 pthread_mutex_timedlock)。

四、如何避免死锁?

死锁的概念

多个进程或线程访问一组竟态资源的时候,出现的永久阻塞的问题。

也可以这么说:指两个或两个以上的线程或进程在执行程序的过程中,因争夺资源或者程序推进顺序不当而相互等待的一个现象

产生的原因 主要有三个:系统资源不足,程序运行推进的顺序不当,资源分配不当。

或者说:死锁产生的必要条件 是:互斥条件、请求和保持条件、不剥夺条件、环路等待条件.

避免死锁 就是打破这四个条件中的某一个即可。如某个进程申请多个资源,只要有一个资源不满足暂时就不要分配任何资源,等所有资源能满足时一起分配。

例方法1:破坏循环等待(锁顺序)

所有线程按照相同的顺序获取锁。

cpp 复制代码
// 线程1和线程2都先锁A再锁B
void* thread1(void* arg) {
    pthread_mutex_lock(&lockA);
    pthread_mutex_lock(&lockB);
    // ...
}

void* thread2(void* arg) {
    pthread_mutex_lock(&lockA);  // 与线程1顺序一致
    pthread_mutex_lock(&lockB);
    // ...
}

例方法2:破坏持有并等待(一次性申请)

使用 trylock 或一次性申请所有资源。

cpp 复制代码
void* thread(void* arg) {
    while (1) {
        pthread_mutex_lock(&lockA);
        if (pthread_mutex_trylock(&lockB) == 0) {
            break;  // 同时获得两个锁
        }
        pthread_mutex_unlock(&lockA); // 释放锁A,避免死锁
        usleep(100); // 稍后重试
    }
    // 操作...
    pthread_mutex_unlock(&lockB);
    pthread_mutex_unlock(&lockA);
}

例方法3:破坏不可剥夺(超时)

使用带超时的锁函数,超时后释放已持有的锁

使用 C++11 标准库的 timed_mutex。

std::timed_mutex:提供 try_lock_for(相对时间)和 try_lock_until(绝对时间)方法。

示例1:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

// 使用 timed_mutex 替代普通的 mutex
std::timed_mutex mtx1, mtx2;

void thread_func() {
    // 尝试获取第一个锁,等待1秒
    if (mtx1.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "线程 " << std::this_thread::get_id() 
                  << " 获得锁1" << std::endl;
        
        // 模拟一些操作
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        
        // 尝试获取第二个锁,等待1秒
        if (mtx2.try_lock_for(std::chrono::seconds(1))) {
            std::cout << "线程 " << std::this_thread::get_id() 
                      << " 获得锁2,执行操作" << std::endl;
            
            // 临界区操作
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            
            mtx2.unlock();  // 释放锁2
        } else {
            std::cout << "线程 " << std::this_thread::get_id() 
                      << " 获取锁2超时,释放锁1" << std::endl;
        }
        
        mtx1.unlock();  // 释放锁1
    } else {
        std::cout << "线程 " << std::this_thread::get_id() 
                  << " 获取锁1超时" << std::endl;
    }
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    
    t1.join();
    t2.join();
    
    return 0;
}

示例2:使用 try_lock_until 指定绝对时间点

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex mtx1, mtx2;

void thread_func() {
    auto timeout_point = std::chrono::steady_clock::now() 
                        + std::chrono::seconds(2);
    
    // 尝试在指定时间点前获取锁
    if (mtx1.try_lock_until(timeout_point)) {
        std::cout << "线程 " << std::this_thread::get_id() 
                  << " 获得锁1" << std::endl;
        
        // 更新超时点
        timeout_point = std::chrono::steady_clock::now() 
                       + std::chrono::seconds(1);
        
        if (mtx2.try_lock_until(timeout_point)) {
            std::cout << "线程 " << std::this_thread::get_id() 
                      << " 获得锁2" << std::endl;
            
            // 临界区
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            
            mtx2.unlock();
        }
        mtx1.unlock();
    }
}
相关推荐
桌面运维家1 小时前
理解 Linux Front Page:构建动态Web首页指南
linux·运维·服务器
金士镧(厦门)新材料有限公司2 小时前
氧化镧:现代工业的重要稀土材料
人工智能·科技·安全·全文检索·生活·能源
季明洵2 小时前
预处理详解(上)
linux·c语言·数据结构·预定义
ShoreKiten2 小时前
DC-3靶机渗透--CTFer从0到1的进阶之路
安全·网络安全·渗透测试
I love studying!!!2 小时前
python项目: 下载数据
开发语言·python
toooooop82 小时前
linux常用命令nano和vim有啥区别
linux·运维·vim
不只会拍照的程序猿2 小时前
《嵌入式AI筑基笔记03:Python流程控制,从C的严谨到Python的简洁》
c语言·开发语言·笔记·python
问今域中2 小时前
java技术史001:EJB 侵入性的历史阵痛与 Spring 的突围
java·开发语言·rpc
BUG创建者2 小时前
openlayers上跟据经纬度画出轨迹
开发语言·javascript·vue·html