Linux多线程:互斥锁与信号量学习笔记

一、线程共享资源基础

同一进程下的多个线程:

  • 共享:文本段(代码段)、数据段、堆区、文件描述符
  • 私有:每个线程独立栈区、寄存器、线程局部数据

共享内存带来便利的同时,会产生资源竞争(竞态条件):多个线程同时读写同一份共享数据,会出现数据错乱。

基础概念

  1. 原子操作:不能被 CPU 调度打断的最小操作,一旦开始就必须执行完毕。

注意:C 语言普通 i++ 不是原子操作,分为读‑改‑写三步,多线程下会出错。

  1. 临界区(临界代码):被加锁、解锁包裹的代码片段。同一时刻只允许一个线程执行临界区代码,用来保护共享资源。

二、互斥锁 pthread_mutex_t

核心作用

实现共享资源互斥访问,保证同一时刻仅有一个线程进入临界区。把非原子的读‑改‑写操作包装成逻辑上不可分割的整体,解决竞态条件,保证并发下数据一致性。

锁的代价

✅ 优点:把非线程安全的并发代码变为安全,防止脏读、数据丢失更新,可以控制执行顺序。

❌ 代价:带来性能开销;使用不当会造成死锁,程序并发能力下降。

互斥锁工作原理

互斥锁依靠底层原子操作实现排他性。一个线程成功加锁后,其他线程再次加锁会被操作系统阻塞,让出 CPU;直到持有锁的线程执行完临界区调用解锁,操作系统才唤醒等待的线程。

  1. 一个进程可以创建任意数量互斥锁
  2. 普通互斥锁不支持重复加锁 :同一个线程拿到锁之后,再次调用 lock 会直接卡死;必须先解锁,才能再次加锁。

错误示例:锁加完,条件分支内才解锁,分支走不到就永久死锁

c 复制代码
pthread_mutex_lock(&lock);
if (0) {
    pthread_mutex_unlock(&lock); // 永远执行不到,锁不释放,程序卡死
}

API 接口

头文件:#include <pthread.h>

  1. 定义锁(一般全局或者堆上分配)
c 复制代码
pthread_mutex_t lock;
  1. 初始化锁
c 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:锁变量地址
  • attr:锁属性,传 NULL 使用默认属性
  • 返回:0 成功,非 0 代表错误
  1. 上锁(阻塞版)
c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

拿不到锁就阻塞休眠,直到锁被释放。

  1. 解锁
c 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

注意:只有持有锁的线程才可以解锁,其他线程调用会报错 EPERM

  1. 销毁锁
c 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

如果锁还被占用,销毁返回 EBUSY

三种加锁方式对比

函数 特性 适用场景
pthread_mutex_lock() 阻塞,拿不到锁就休眠 绝大多数常规业务
pthread_mutex_trylock() 非阻塞,拿不到锁立刻返回,不阻塞 不想卡死,拿不到锁就做别的逻辑
pthread_mutex_timedlock() 带超时的阻塞等待 防止死锁,设置最大等待时间

死锁

死锁:多个线程互相持有对方需要的锁,全部线程阻塞,程序不再推进。

死锁四个必要条件(全部满足才会死锁)
  1. 互斥条件:资源同一时刻只能一个线程使用
  2. 不可剥夺条件:已经占有的锁,别人不能强行抢走
  3. 请求与保持:线程已经持有锁,同时申请别的锁
  4. 循环等待:线程之间形成锁的循环依赖
避免死锁方案
  1. 使用 trylock 尝试加锁,失败就释放已经拿到的锁
  2. 所有线程保持统一的加锁顺序,不要交叉申请锁

三、信号量 sem_t

信号量本质代表有限数量的资源 ,可以用来做互斥,也可以做线程间同步。

互斥锁只允许最多 1 个线程进入临界区;信号量初始值可以大于 1,允许多个线程同时访问资源。

头文件:#include <semaphore.h>

API

  1. sem_init 初始化信号量
c 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:信号量变量指针
  • pshared0 用于线程之间;非 0 用于进程之间(搭配共享内存)
  • value:初始可用资源数目
  • 返回:0 成功,‑1 失败,设置 errno
  1. sem_destroy 销毁信号量
c 复制代码
int sem_destroy(sem_t *sem);
  1. sem_wait():申请资源,信号量‑1
c 复制代码
int sem_wait(sem_t *sem);

信号量 > 0:直接减 1 返回;信号量等于 0:线程阻塞等待,直到有资源释放。

  1. sem_post():释放资源,信号量+1
c 复制代码
int sem_post(sem_t *sem);

原子性把信号量 +1,同时唤醒一个正在等待该信号量的线程。

小提示:

  • 信号量初始值设为 1,效果等价互斥锁,可以实现互斥访问;
  • 初始值大于 1,实现"允许 N 个线程同时访问资源"。

互斥锁 vs 信号量简单区分

  1. 互斥锁(mutex):专门做互斥,谁加锁,谁必须负责解锁;所有权概念。
  2. 信号量(sem_t) :资源计数器,没有所有者概念,任意线程都可以 post 释放资源,既可以互斥,也可以做线程同步、生产者消费者模型。

编译提示

使用线程库编译,必须链接 pthread 库

bash 复制代码
gcc demo.c -o demo -pthread

笔记要点记忆:

  • mutex:锁,有所有者,重复加锁直接死锁;
  • sem_wait 减资源,sem_post 加资源,无所有者;
  • 死锁四个条件,统一加锁顺序是最常用规避手段。
相关推荐
光电笑映15 小时前
网络通信基础:从协议分层到 Socket 编程预备
linux·运维·服务器·网络
传奇开心果编程15 小时前
【Rust入门练中学】 第3课:数据类型
开发语言·学习·rust
酸菜。15 小时前
dw_apb_gpio总结
linux
humors22115 小时前
今年我正在看、看完和还没看的书单(不定期更新)
学习·阅读·读书·书单·书籍
wunaiqiezixin16 小时前
MIT 6.S081 file system 实验篇(lab9):Large files (moderate)
linux·unix·os·xv6·mit6.s081
泡泡鱼(敲代码中)16 小时前
MySQL 学习笔记:DCL、函数与约束 —— 安全、效率、完整性的三板斧
开发语言·笔记·sql·学习·mysql·gitee
GeW16 小时前
RHCE 考试通过率提升:核心考点、实验环境与排错技巧
linux
爱吃苹果的日记本16 小时前
数据结构第五课
数据结构·学习
吴声子夜歌17 小时前
Shell编程实例——与解析相关的任务(二)
linux·运维·shell
mengge.cloud17 小时前
0917Docker 小白实验教程(理论+实操)
linux·服务器·网络·docker