news 2026/2/28 12:23:20

Linux 线程控制核心:互斥锁与信号量(同步)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 线程控制核心:互斥锁与信号量(同步)

一、互斥锁:临界资源的排他性访问

1.1 核心概念

(1)临界资源

多线程中会被读写操作的共享资源,常见类型:

  • 全局变量、静态变量;
  • 文件、设备(如串口、网卡);
  • 其他可被多线程访问的共享内存 / 句柄。
(2)排他性访问

同一时刻,仅允许一个线程对临界资源进行读写操作 —— 这是互斥锁的核心目标。

(3)为什么需要互斥?

多线程并发执行时,代码是「穿插调度」的。以简单的a++为例:

c

运行

a++; // 看似一行代码,汇编至少分3步: // 1. 从内存读取a的值到寄存器; // 2. 寄存器中a的值+1; // 3. 寄存器值写回内存a。

若线程 1 执行完前 2 步后被调度切走,线程 2 执行完整 3 步,再切回线程 1 执行第 3 步 —— 最终a只加了 1 次,而非预期的 2 次,导致数据一致性错误

互斥锁的作用就是将这段代码变成「原子操作」:加锁后,这段代码必须在一次线程调度中完整执行,不可被打断。

1.2 互斥锁的使用步骤

互斥锁的生命周期遵循「定义→初始化→加锁→解锁→销毁」的固定流程,缺一不可。

(1)核心函数(POSIX 标准)
操作函数原型关键说明
定义pthread_mutex_t mutex;声明互斥锁变量(全局 / 局部均可,需保证所有线程可见)
初始化int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);attr 传 NULL 表示默认属性;成功返回 0,失败返回非 0
加锁int pthread_mutex_lock(pthread_mutex_t *mutex);加锁失败则阻塞(等待其他线程解锁);成功返回 0
解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);必须与加锁线程为同一个;成功返回 0
销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);锁未解锁时销毁会报错;成功返回 0
(2)核心规则
  • 加锁和解锁必须由同一个线程执行;
  • 临界区(加锁→解锁之间的代码)必须「短小精悍」:
    • 禁止在临界区中执行sleep、IO 等耗时操作;
    • 临界区代码越短,多线程并发效率越高。
(3)基础示例:互斥锁保护全局变量

c

运行

#include <stdio.h> #include <pthread.h> #include <unistd.h> int a = 0; pthread_mutex_t mutex; void* th_func(void* arg) { for (int i = 0; i < 10000; i++) { pthread_mutex_lock(&mutex); // 加锁 a++; // 临界区:仅一行,原子操作 pthread_mutex_unlock(&mutex); // 解锁 } return NULL; } int main() { pthread_t tid1, tid2; pthread_mutex_init(&mutex, NULL); // 初始化锁 pthread_create(&tid1, NULL, th_func, NULL); pthread_create(&tid2, NULL, th_func, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); // 销毁锁 printf("最终a的值:%d\n", a); // 预期输出20000 return 0; }

二、信号量:带顺序的同步访问

2.1 核心概念

(1)同步的定义

「有先后顺序的排他性访问」—— 不仅要保证同一时刻一个资源被一个线程访问,还要强制线程按照指定顺序执行(如线程 A 执行完后,线程 B 才能执行)。

(2)与互斥锁的关系
  • 互斥包含同步:同步是互斥的「特例」(互斥只保证排他,同步额外保证顺序);
  • 释放逻辑不同:
    • 互斥锁:加锁 / 解锁是同一个线程;
    • 信号量:线程 A 释放资源,线程 B 申请资源(交叉释放)。
(3)计数信号量的用途

信号量初始值可大于 1(如 3、5),适用于「多份相同资源」的竞争场景(如你之前代码中win=3的有限资源竞争)。

2.2 信号量的使用步骤

信号量生命周期:「定义→初始化→PV 操作→销毁」。

(1)核心函数(POSIX 标准)
操作函数原型关键说明
定义sem_t sem;声明信号量变量
初始化int sem_init(sem_t *sem, int pshared, unsigned int value);pshared=0(线程间使用)、!=0(进程间);value = 初始资源数;成功返回 0,失败 - 1
P 操作(申请资源)int sem_wait(sem_t *sem);sem>0 则 sem-1 并继续;sem=0 则阻塞;成功返回 0,失败 - 1
V 操作(释放资源)int sem_post(sem_t *sem);sem+1,不阻塞;成功返回 0,失败 - 1
销毁int sem_destroy(sem_t *sem);成功返回 0,失败 - 1
(2)核心规则
  • P 操作(sem_wait):申请资源,资源数 - 1;
  • V 操作(sem_post):释放资源,资源数 + 1;
  • 信号量临界区可包含短暂休眠 / 耗时操作(比互斥锁灵活)。
(3)基础示例:信号量实现线程同步

c

运行

#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> sem_t sem; // 信号量:控制线程执行顺序 void* th1(void* arg) { printf("线程1:执行初始化操作\n"); sleep(2); sem_post(&sem); // V操作:释放资源(sem=1) return NULL; } void* th2(void* arg) { sem_wait(&sem); // P操作:等待资源(sem=0时阻塞) printf("线程2:线程1初始化完成后执行\n"); return NULL; } int main() { pthread_t tid1, tid2; sem_init(&sem, 0, 0); // 初始化:线程间使用,初始值0 pthread_create(&tid1, NULL, th1, NULL); pthread_create(&tid2, NULL, th2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&sem); return 0; }

三、互斥锁 vs 信号量(核心区别)

维度互斥锁信号量
核心目标排他性访问(无顺序)有序的排他性访问(同步)
释放主体加锁线程自己解锁线程 A 申请,线程 B 释放(交叉)
初始值无(只有锁定 / 未锁定状态)可设为任意非负整数(如 0、1、3)
临界区限制禁止耗时操作,必须短小可包含短暂休眠 / 小耗时操作
适用场景单一资源的排他访问(如全局变量)多资源竞争、线程顺序控制

四、死锁:线程控制的 “致命陷阱”

4.1 死锁的定义

因锁资源的申请 / 释放逻辑错误,导致线程 / 进程永久阻塞,无法继续执行的现象。

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

  1. 互斥条件:一个资源每次只能被一个线程使用;
  2. 请求与保持条件:线程因申请资源阻塞时,不释放已持有的资源;
  3. 不剥夺条件:线程已获得的资源,未使用完前不能被强行剥夺;
  4. 循环等待条件:多个线程形成「A 等 B 的资源,B 等 C 的资源,C 等 A 的资源」的循环。

4.3 死锁规避技巧

  1. 锁的申请顺序一致:所有线程按「锁 1→锁 2→锁 3」的顺序申请,避免循环等待;
  2. 加锁限时:使用pthread_mutex_timedlock替代pthread_mutex_lock,超时则放弃;
  3. 避免嵌套锁:尽量减少锁的嵌套使用,嵌套越多,死锁风险越高;
  4. 及时解锁:临界区执行完毕立即解锁,不持有锁休眠。

五、实战场景:有限资源的多线程竞争

以你之前的「win=3 资源竞争」场景为例,用信号量替代互斥锁,更贴合 “多资源竞争” 的需求:

c

运行

#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> #include <stdlib.h> #include <time.h> int a = 0; sem_t sem; // 信号量:初始值3,代表3个可用资源 void* th_func(void* arg) { while (1) { sem_wait(&sem); // P操作:申请资源(资源数-1) // 临界区:可包含短暂休眠 printf("线程%lu:获取资源\n", pthread_self()); sleep(rand() % 2); a++; printf("线程%lu:释放资源,a=%d\n", pthread_self(), a); sem_post(&sem); // V操作:释放资源(资源数+1) if (a >= 10) break; // 退出条件 } return NULL; } int main() { pthread_t tid[10]; srand((unsigned int)time(NULL)); sem_init(&sem, 0, 3); // 初始化:线程间使用,初始资源数3 for (int i = 0; i < 10; i++) { pthread_create(&tid[i], NULL, th_func, NULL); } for (int i = 0; i < 10; i++) { pthread_join(tid[i], NULL); } sem_destroy(&sem); printf("最终a的值:%d\n", a); return 0; }

六、核心总结

  1. 互斥锁:解决「单一资源的排他访问」,临界区必须短小,加解锁同线程;
  2. 信号量:解决「多资源竞争 / 线程同步」,支持交叉释放,临界区可适度耗时;
  3. 死锁规避:打破四个必要条件中的任意一个(如统一锁申请顺序)即可;
  4. 选型原则:
    • 单一资源排他访问 → 互斥锁;
    • 多资源竞争 / 线程顺序控制 → 信号量。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/26 4:42:41

WindSend跨设备文件传输终极指南:快速上手完整教程

WindSend跨设备文件传输终极指南&#xff1a;快速上手完整教程 【免费下载链接】WindSend Quickly and securely sync clipboard, transfer files and directories between devices. 快速安全的同步剪切板&#xff0c;传输文件或文件夹 项目地址: https://gitcode.com/gh_mir…

作者头像 李华
网站建设 2026/2/6 13:29:40

智能科学与技术毕设新颖的项目选题建议

文章目录&#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f;1.2 选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f;&#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 大数据电商用户行为…

作者头像 李华