Linux系统 -- 多线程的控制(互斥与同步)

发布于:2025-08-29 ⋅ 阅读:(20) ⋅ 点赞:(0)

在多线程编程中,多个线程可能同时访问临界资源(如共享变量、文件、硬件设备等),若缺乏控制会导致数据混乱。互斥同步是解决该问题的核心机制,其中互斥锁保证临界资源的排他访问,信号量实现线程间的有序协作。以下是核心知识点整理:

一、互斥(Mutex):保证临界资源的排他访问

1. 核心概念

  • 临界资源:多线程共同访问的公共资源(如全局变量、共享内存、文件句柄)。
  • 互斥:对临界资源的排他性访问—— 同一时间仅允许一个线程访问,其他线程需阻塞等待,直到当前线程释放资源。
  • 互斥锁:实现互斥机制的核心工具,本质是 “锁”,通过 “加锁 - 访问 - 解锁” 的流程控制临界资源的访问权限。

2. 互斥锁的使用流程

互斥锁的使用需遵循 “定义→初始化→加锁→解锁→销毁” 的五步流程,所有操作均需包含 <pthread.h> 头文件。

(1)定义互斥锁

声明一个 pthread_mutex_t 类型的变量,代表一个互斥锁:

pthread_mutex_t mutex;  // 定义互斥锁
(2)初始化互斥锁

通过 pthread_mutex_init 函数初始化已定义的互斥锁,设置锁的属性。

函数原型 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
功能 初始化指定的互斥锁,分配锁的资源
参数 mutex:指向待初始化的互斥锁变量的指针
attr:锁的属性(通常设为 NULL,表示使用默认属性)
返回值 - 成功:返回 0
- 失败:返回非 0(错误码,可通过 perror 打印错误信息)
// 初始化互斥锁,使用默认属性
if (pthread_mutex_init(&mutex, NULL) != 0) {
    perror("pthread_mutex_init failed");
    exit(1);
}
(3)加锁(P 操作的简化)

通过 pthread_mutex_lock 函数为临界区 “上锁”,确保同一时间仅一个线程进入临界区。

函数原型 int pthread_mutex_lock(pthread_mutex_t *mutex);
功能 尝试获取互斥锁:
- 若锁未被占用,立即获取并继续执行
- 若锁已被占用,当前线程阻塞等待
参数 mutex:指向待加锁的互斥锁变量的指针
返回值 - 成功:返回 0
- 失败:返回非 0

核心注意事项

  • 加锁后到解锁前的代码段称为 临界区,该区域的操作具有 原子性(CPU 多条指令不可分割,必须一次性执行完毕)。
  • 其他线程在临界区未解锁时,调用 pthread_mutex_lock 会阻塞,直到锁被释放。
  • 临界区需 尽可能小(仅包含访问临界资源的代码),避免因耗时操作导致其他线程长期阻塞。
(4)解锁(V 操作的简化)

通过 pthread_mutex_unlock 函数释放互斥锁,允许其他阻塞的线程获取锁并进入临界区。

函数原型 int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能 释放已获取的互斥锁,唤醒等待该锁的线程(若有)
参数 mutex:指向待解锁的互斥锁变量的指针
返回值 - 成功:返回 0
- 失败:返回非 0(如未加锁却解锁)

注意:解锁操作必须与加锁操作成对出现,且需在同一线程中执行(不可跨线程解锁)。

(5)销毁互斥锁

互斥锁不再使用时,需通过 pthread_mutex_destroy 函数销毁,释放锁占用的系统资源。

函数原型 int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能 销毁互斥锁,释放其占用的内存和系统资源
参数 mutex:指向待销毁的互斥锁变量的指针
返回值 - 成功:返回 0
- 失败:返回非 0(如锁未初始化或仍被占用)
// 销毁互斥锁
if (pthread_mutex_destroy(&mutex) != 0) {
    perror("pthread_mutex_destroy failed");
    exit(1);
}

3. 互斥锁的关键特性

  • 排他性:同一时间仅一个线程持有锁,确保临界资源的独占访问
  • 阻塞性pthread_mutex_lock 会阻塞等待,直到锁可用(避免 CPU 空转)。
  • 单一对应:一个互斥锁通常对应一个临界资源(若多个资源无关联,需定义多个锁)。

4. 互斥锁的应用举例

1、计数器保护

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

pthread_mutex_t mutex;  // 定义互斥锁
int count = 0;  // 共享资源(临界资源)

// 线程函数:对共享计数器进行5000次递增
void* th(void* arg) {
    int i = 5000;
    while (i--) {
        pthread_mutex_lock(&mutex);  // 加锁(进入临界区)
        int tmp = count;  // 读取当前值
        printf("count:%d\n", tmp + 1);  // 打印新值
        count = tmp + 1;  // 更新计数器
        pthread_mutex_unlock(&mutex);  // 解锁(退出临界区)
    }
    return NULL;
}

int main(int argc, char** argv) {
    pthread_t tid1, tid2;
    pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁
    
    // 创建两个线程
    pthread_create(&tid1, NULL, th, NULL);
    pthread_create(&tid2, NULL, th, NULL);

    // 等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    pthread_mutex_destroy(&mutex);  // 销毁互斥锁
    return 0;
}

注:输出严格按1-10000顺序递增,无重复或跳号,证明互斥锁有效保护了共享资源

    2、资源池管理

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

int WIN = 3;  // 可用资源数量(临界资源)
pthread_mutex_t mutex;  // 互斥锁

// 线程函数:模拟资源获取与释放
void* th(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);  // 加锁
        if (WIN > 0) {  // 检查资源可用性
            WIN--;  // 占用资源
            pthread_mutex_unlock(&mutex);  // 解锁(允许其他线程检查)
            
            printf("get win...\n");  // 获取资源成功
            sleep(rand() % 5 + 1);  // 模拟资源使用时间(1-5秒)
            printf("release win...\n");  // 释放资源
            
            pthread_mutex_lock(&mutex);  // 重新加锁
            WIN++;  // 释放资源
            pthread_mutex_unlock(&mutex);  // 解锁
            break;
        } else {
            pthread_mutex_unlock(&mutex);  // 无资源时释放锁
        }
    }
    return NULL;
}

int main(int argc, char** argv) {
    pthread_t tid[10] = {0};
    srand(time(NULL));  // 初始化随机种子
    pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁
    
    // 创建10个线程
    for (int i = 0; i < 10; i++) {
        pthread_create(&tid[i], NULL, th, NULL);
    }

    // 等待所有线程结束
    for (int i = 0; i < 10; i++) {
        pthread_join(tid[i], NULL);
    }
    
    pthread_mutex_destroy(&mutex);  // 销毁互斥锁
    return 0;
}

注:任意时刻最多3个线程同时持有资源,资源获取/释放顺序可能不同,但不会出现超过3个"get win"连续出现
 

二、同步(Semaphore):保证临界资源的有序访问

1. 核心概念

  • 同步:在互斥的基础上,进一步要求线程按 特定顺序 访问临界资源(如 “生产者先生产,消费者后消费”)。
  • 信号量:实现同步机制的工具,本质是一个 “计数器”,通过 PV 操作(申请 / 释放资源)控制线程的执行顺序。
  • 分类
    • 无名信号量:用于 线程间同步(共享内存空间中的信号量,仅同一进程内的线程可见)。
    • 有名信号量:用于 进程间同步(通过文件系统中的名称标识,不同进程可访问)。

2. 信号量的使用流程(以无名信号量为例)

信号量的使用需遵循 “定义→初始化→PV 操作→销毁” 的流程,所有操作均需包含 <semaphore.h> 头文件。

(1)定义信号量

声明一个 sem_t 类型的变量,代表一个信号量:

sem_t sem;  // 定义无名信号量
(2)初始化信号量

通过 sem_init 函数初始化已定义的信号量,设置信号量的类型和初始值。

函数原型 int sem_init(sem_t *sem, int pshared, unsigned int value);
功能 初始化信号量,设置其共享范围和初始计数
参数 sem:指向待初始化的信号量变量的指针
pshared:共享范围(0 表示线程间使用,非0 表示进程间使用)
value:信号量初始值(二值信号量通常设为 0 或 1,计数信号量可设为大于 1 的值)
返回值 - 成功:返回 0
- 失败:返回 -1(错误码,可通过 perror 打印)

关键说明

  • 二值信号量:初始值为 0 或 1,对应 “资源不可用” 或 “资源可用”,常用于同步。
  • 计数信号量:初始值大于 1,表示可同时允许 value 个线程访问资源。
// 初始化线程间使用的二值信号量,初始值为 0(资源不可用)
if (sem_init(&sem, 0, 0) != 0) {
    perror("sem_init failed");
    exit(1);
}
(3)PV 操作(核心)

信号量的核心是 PV 操作,通过 “申请资源(P)” 和 “释放资源(V)” 控制线程执行顺序。

① P 操作(申请资源):sem_wait

尝试获取信号量对应的资源,若资源不足则阻塞等待。

函数原型 int sem_wait(sem_t *sem);
功能 申请资源:
- 若信号量计数 value > 0,则 value -= 1,线程继续执行
- 若信号量计数 value == 0,线程阻塞等待,直到其他线程执行 V 操作
参数 sem:指向待操作的信号量变量的指针
返回值 - 成功:返回 0
- 失败:返回 -1
② V 操作(释放资源):sem_post

释放信号量对应的资源,唤醒阻塞等待的线程(若有)。

函数原型 int sem_post(sem_t *sem);
功能 释放资源:
- 信号量计数 value += 1(无论当前值是否为 0
- 若有线程因该信号量阻塞,唤醒其中一个线程
参数 sem:指向待操作的信号量变量的指针
返回值 - 成功:返回 0
- 失败:返回 -1
(4)销毁信号量

信号量不再使用时,通过 sem_destroy 函数销毁,释放其占用的系统资源。

函数原型 int sem_destroy(sem_t *sem);
功能 销毁信号量,释放其占用的内存和系统资源
参数 sem:指向待销毁的信号量变量的指针
返回值 - 成功:返回 0
- 失败:返回 -1(如信号量未初始化或仍有线程等待)

3. 信号量的关键特性

  • 有序性:通过 PV 操作的先后顺序,强制线程按预期流程执行(如 “先生产后消费”)。
  • 灵活性:支持二值信号量(同步)和计数信号量(资源限流),适用场景更广。
  • 跨线程 / 进程:无名信号量支持线程间同步,有名信号量支持进程间同步。

三、互斥与同步的对比

维度 互斥(Mutex) 同步(Semaphore)
核心目标 保证临界资源的排他访问(防数据混乱) 保证线程按特定顺序访问资源(防执行顺序错误)
顺序要求 无顺序要求(只要排他即可) 有明确顺序要求(如 “先 A 后 B”)
操作主体 加锁和解锁必须在同一线程中执行 PV 操作通常在不同线程中执行(如生产者 V、消费者 P)
资源计数 仅支持 “0/1” 二值(锁占用 / 未占用) 支持二值(0/1)或计数(>1)
典型场景 多线程修改同一全局变量、共享文件写操作 生产者 - 消费者模型、任务队列的先后执行
关系 同步是 “有顺序要求的互斥”,互斥是同步的基础 基于互斥,进一步解决顺序问题

四、死锁(Deadlock):成因、必要条件与解决方案

1. 核心概念

  • 死锁:多个线程因互相等待对方持有的资源,导致所有线程永久阻塞(代码停滞,无法继续执行)。

2. 死锁的成因

  1. 系统资源不足:多个线程争夺有限的资源(如互斥锁、信号量)。
  2. 进程推进顺序不当:线程按错误的顺序申请 / 释放资源(如线程 A 先锁资源 1,线程 B 先锁资源 2,再互相申请对方的资源)。
  3. 资源分配不当:资源分配策略未考虑线程的依赖关系(如一次性分配所有资源,或不允许资源抢占)。

3. 死锁的四个必要条件(缺一不可)

只有同时满足以下四个条件,才会发生死锁:

  1. 互斥条件:资源只能被一个线程占用(如互斥锁的排他性)。
  2. 请求与保持条件:线程持有已获得的资源,同时申请新的资源;若新资源不可得,不释放已持有的资源。
  3. 不剥夺条件:线程已获得的资源,在未主动释放前,不能被其他线程强行剥夺。
  4. 循环等待条件:多个线程形成 “循环依赖”(如线程 A 等线程 B 的资源,线程 B 等线程 A 的资源)。

4. 死锁的解决方案

(1)避免死锁:破坏必要条件
  • 破坏 “请求与保持”:线程申请资源前,先释放已持有的所有资源(如 “一次性申请所有资源,否则不申请”)。
  • 破坏 “循环等待”:按固定顺序申请资源(如所有线程均先申请资源 1,再申请资源 2)。
  • 破坏 “不剥夺”:允许线程在超时后释放已持有的资源(如使用非阻塞锁)。
(2)解决死锁:使用非阻塞锁 / 信号量

当线程申请资源时,若资源不可用,不阻塞等待,而是立即返回错误,避免死锁。常用函数如下:

① 非阻塞互斥锁:pthread_mutex_trylock
函数原型 int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能 尝试获取互斥锁:
- 若锁未被占用,立即加锁并返回 0
- 若锁已被占用,不阻塞,直接返回非 0(错误码)
参数 mutex:指向待加锁的互斥锁变量的指针
返回值 - 成功:返回 0
- 失败:返回非 0(如锁已被占用)
// 尝试加锁,若失败则重试或处理
if (pthread_mutex_trylock(&mutex) != 0) {
    printf("锁已被占用,稍后重试\n");
    sleep(1);  // 延迟后重试
    // 或直接放弃,避免阻塞
}
② 非阻塞信号量:sem_trywait
函数原型 int sem_trywait(sem_t *sem);
功能 尝试申请信号量资源:
- 若资源可用(value > 0),value -= 1 并返回 0
- 若资源不可用(value == 0),不阻塞,直接返回 -1
参数 sem:指向待操作的信号量变量的指针
返回值 - 成功:返回 0
- 失败:返回 -1

五、总结

  1. 互斥锁是 “排他工具”,解决 “多线程抢资源” 的问题,确保临界资源的独占访问。
  2. 信号量是 “有序工具”,解决 “多线程按顺序执行” 的问题,在互斥基础上实现同步。
  3. 死锁是多线程编程的常见陷阱,需通过 “破坏必要条件” 或 “使用非阻塞操作” 避免。
  4. 实际开发中,需根据场景选择工具:仅需排他用互斥锁,需顺序控制用信号量;同时注意临界区最小化、资源申请顺序统一,减少死锁风险。

网站公告

今日签到

点亮在社区的每一天
去签到