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,使用规范
相关推荐
南境十里·墨染春水2 小时前
linux学习进展 线程
java·linux·学习
HABuo2 小时前
【linux网络基础(二)】理解端口号&UDP、TCP协议&网络字节序
linux·服务器·c语言·网络·c++·ubuntu·centos
爱学习的小囧2 小时前
ESXi 存储路径丢失(PDL/APD)完整处置教程:分清类型再操作,一步不踩坑
linux·运维·服务器·网络·esxi·vmware
不做超级小白2 小时前
Termux 完整安装与配置指南(2026.4.24最新版,从零到可用)
linux·手机
Lumos_7772 小时前
Linux -- 信号
linux·运维·服务器
leikooo2 小时前
Skills 实战:Unsplash → COS 自动化配图
运维·ai·自动化
Lumos_7772 小时前
Linux -- 管道
linux·运维·服务器
哦哦~9212 小时前
揭示多功能合成界面,增强致密厚复合电极的机械和电化学性能
服务器·网络·人工智能
宇宙realman_9992 小时前
DSP28335-FlashAPI使用
linux·前端·python