Linux多线程编程(二):互斥锁、线程安全与死锁剖析

目录

    • 一、为什么需要同步?
      • [1. 临界资源与临界区](#1. 临界资源与临界区)
      • [2. 互斥与原子性](#2. 互斥与原子性)
      • [3. 竞态条件示例 ------ 售票系统](#3. 竞态条件示例 —— 售票系统)
    • 二、互斥量(Mutex)
      • [1. 初始化与销毁](#1. 初始化与销毁)
      • [2. 加锁与解锁](#2. 加锁与解锁)
      • [3. 改进售票系统](#3. 改进售票系统)
      • [4. 互斥量的底层原理](#4. 互斥量的底层原理)
    • 三、可重入与线程安全
      • [1. 概念区分](#1. 概念区分)
      • [2. 常见不安全与不可重入情况](#2. 常见不安全与不可重入情况)
      • [3. 联系与区别](#3. 联系与区别)
    • 四、死锁
      • [1. 定义](#1. 定义)
      • [2. 四个必要条件](#2. 四个必要条件)
      • [3. 避免死锁的方法](#3. 避免死锁的方法)
    • [五、条件变量(Condition Variable)](#五、条件变量(Condition Variable))
      • [1. 为什么需要条件变量?](#1. 为什么需要条件变量?)
      • [2. 条件变量函数](#2. 条件变量函数)
      • [3. 为什么 `pthread_cond_wait` 需要互斥量?](#3. 为什么 pthread_cond_wait 需要互斥量?)
      • [4. 条件变量使用规范](#4. 条件变量使用规范)
      • [5. 简单示例](#5. 简单示例)
    • 六、总结

一、为什么需要同步?

1. 临界资源与临界区

  • 临界资源:多线程共享的资源(如全局变量、文件、设备等)。
  • 临界区:访问临界资源的代码段,需要互斥执行。

2. 互斥与原子性

  • 互斥:保证同一时刻只有一个线程进入临界区。
  • 原子操作:不可被中断的操作,要么全部完成,要么全部未完成。

3. 竞态条件示例 ------ 售票系统

下面的代码模拟了四个线程同时售卖 100 张票,未加任何保护:

c 复制代码
int ticket = 100;

void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        if (ticket > 0) {
            usleep(1000);           // 模拟业务处理
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
}

运行结果可能输出负数票(如 -1、-2),这明显是错误的。原因在于:

  • ticket > 0 判断与 ticket-- 操作不是原子的。
  • 汇编层面,ticket-- 对应三条指令:load(加载到寄存器)、update(减1)、store(写回内存)。
  • 线程可能在执行完 load 后被切换,导致其他线程读到旧值,最终超卖。

结论:必须引入同步机制,保证对临界资源的互斥访问。


二、互斥量(Mutex)

互斥量是 Linux 中最基本的互斥锁,用于保护临界区。

1. 初始化与销毁

静态初始化(适用于全局或静态变量):

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化

c 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// attr 传 NULL 使用默认属性

销毁

c 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:使用静态初始化的互斥量不需要销毁;不要销毁已加锁的互斥量。

2. 加锁与解锁

c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);   // 阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 非阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • lock:若锁已被占用,则阻塞等待。
  • unlock:释放锁,唤醒等待的线程。

3. 改进售票系统

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

int ticket = 100;
pthread_mutex_t mutex;

void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        } else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return NULL;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

现在,票数永远不会出现负数,因为每次只有一个线程进入临界区。

4. 互斥量的底层原理

互斥量的实现依赖 CPU 提供的原子交换指令(如 x86 的 xchgcmpxchg)。一条指令完成寄存器和内存单元的交换,保证"测试并设置"的原子性。多核平台上,总线锁机制确保同一时刻只有一个处理器能执行该指令。


三、可重入与线程安全

1. 概念区分

  • 线程安全:多个线程并发执行同一函数,结果可预期,不会出现数据错误。
  • 可重入:函数在被中断后再次被调用(重入),结果仍然正确。

2. 常见不安全与不可重入情况

情况 说明
使用全局或静态变量(未加锁) 线程不安全、不可重入
调用 malloc / free 使用全局堆管理结构,不可重入
调用标准 I/O(printffopen 内部使用全局缓冲区
返回指向静态数据的指针 多线程调用会覆盖数据

3. 联系与区别

  • 可重入函数一定是线程安全的。
  • 线程安全函数不一定是可重入的(例如加锁保护的函数,若重入时锁未释放,会死锁)。
  • 纯局部变量(不访问全局数据)的函数是可重入且线程安全的。

示例

c 复制代码
// 线程安全但不可重入(因为使用了锁)
int safe_inc() {
    static int counter = 0;
    pthread_mutex_lock(&lock);
    counter++;
    pthread_mutex_unlock(&lock);
    return counter;
}

如果在持有锁期间再次调用该函数(同一线程重入),就会死锁。因此加锁函数不是可重入的。


四、死锁

1. 定义

死锁:两个或多个线程互相等待对方持有的资源,导致永远阻塞。

2. 四个必要条件

  1. 互斥:资源一次只能被一个线程占用。
  2. 请求与保持:线程持有资源的同时请求其他资源。
  3. 不剥夺:资源只能由持有者主动释放。
  4. 循环等待:线程间形成循环等待链。

3. 避免死锁的方法

  • 破坏四个必要条件之一(例如,资源一次性分配)。
  • 保持一致的加锁顺序(所有线程按相同顺序获取锁)。
  • 使用 pthread_mutex_trylock 避免阻塞。
  • 使用银行家算法或死锁检测(较复杂,一般用于操作系统内部)。

五、条件变量(Condition Variable)

互斥锁解决了互斥问题,但无法解决"同步"问题 ------ 即让线程按特定顺序执行。条件变量允许线程等待某个条件成立,并在条件满足时被唤醒。

1. 为什么需要条件变量?

考虑生产者-消费者模型:消费者需要等待队列非空才能取数据。如果使用互斥锁 + 轮询,会浪费 CPU;使用条件变量,消费者可以在队列为空时阻塞,直到生产者插入数据后唤醒它。

2. 条件变量函数

c 复制代码
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);   // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待线程

3. 为什么 pthread_cond_wait 需要互斥量?

条件变量必须与互斥量配合使用。

原因在于:

  • 判断条件(如队列是否为空)需要访问共享数据,必须加锁。
  • 如果先解锁再等待,可能会错过信号(在解锁后、等待前,条件可能已被满足并发出信号)。
  • pthread_cond_wait 内部会原子地完成:解锁互斥量 + 阻塞等待 + 被唤醒后重新加锁。

错误用法示例

c 复制代码
pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_mutex_unlock(&mutex);
    pthread_cond_wait(&cond, &mutex);  // 这里 mutex 未锁定,行为未定义
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

上述代码中,解锁与等待不是原子操作,可能丢失信号。

4. 条件变量使用规范

等待线程

c 复制代码
pthread_mutex_lock(&mutex);
while (条件为假) {
    pthread_cond_wait(&cond, &mutex);
}
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);

唤醒线程

c 复制代码
pthread_mutex_lock(&mutex);
设置条件为真;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

注意:使用 while 而非 if 检查条件,因为可能发生虚假唤醒(spurious wakeup)。

5. 简单示例

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

pthread_cond_t cond;
pthread_mutex_t mutex;

void *waiter(void *arg) {
    pthread_mutex_lock(&mutex);
    printf("waiter: waiting...\n");
    pthread_cond_wait(&cond, &mutex);
    printf("waiter: awakened!\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void *signaler(void *arg) {
    sleep(2);
    pthread_mutex_lock(&mutex);
    printf("signaler: sending signal\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, waiter, NULL);
    pthread_create(&t2, NULL, signaler, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行结果:

复制代码
waiter: waiting...
signaler: sending signal
waiter: awakened!

六、总结

知识点 核心内容
同步与互斥 临界资源、临界区、原子性、竞态条件
互斥量 pthread_mutex_lock/unlock,保护临界区
线程安全 多线程并发执行结果可预期
可重入 函数被重入后仍正确,可重入函数一定线程安全
死锁 四个必要条件,避免方法(加锁顺序一致等)
条件变量 配合互斥量实现同步,pthread_cond_wait/signal,使用规范
相关推荐
tedcloud12318 小时前
UI-TARS-desktop部署教程:构建AI桌面自动化系统
服务器·前端·人工智能·ui·自动化·github
AC赳赳老秦21 小时前
供应链专员提效:OpenClaw自动跟踪物流信息、更新库存数据,异常自动提醒
java·大数据·服务器·数据库·人工智能·自动化·openclaw
夏日听雨眠21 小时前
LInux(逻辑地址与物理地址的区别,文件描述符,lseek函数)
linux·运维·网络
哲霖软件1 天前
ERP 赋能非标自动化行业:破解物料与库存管理难题
运维·自动化
无心水1 天前
【Hermes:安全、权限与生产环境】40、运行 Hermes 前的生命线:安全审计清单与 11 个必须检查的配置项
人工智能·安全·mcp协议·openclaw·养龙虾·hermes·honcho
ydyd202604211 天前
制造业数字化干货:设备巡检、报修、保养一体化管理流程拆解
网络
qq_542515411 天前
Ubuntu 22.04.4 LTS安装ToDesk最新版打不开,无响应?旧版本4.7.2_277版本分享
linux·ubuntu·todesk
火车叼位1 天前
替代 Tiny Win10 的 Linux 方案:Debian XFCE 精简桌面搭建
linux·运维
Hali_Botebie1 天前
【图卷积网络】GCN是AXΘ 和CNN是AX
网络·人工智能·cnn
IpdataCloud1 天前
高并发场景下IP数据接口怎么选?从QPS到离线库的完整选型指南
网络·网络协议·tcp/ip