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 加资源,无所有者;
  • 死锁四个条件,统一加锁顺序是最常用规避手段。
相关推荐
重生之我是Java开发战士29 分钟前
【Java EE】认识Linux与项目部署
java·linux·java-ee
小蜗 strong40 分钟前
ATA各个章节
学习
Tisfy1 小时前
LeetCode 3720.大于目标字符串的最小字典序排列:状态机 —— :从左往右枚举,失败则退回(最多退回一次)
linux·数据库·leetcode·字符串·状态机·构造
SKH.1 小时前
Linux软件编程(5)线程
java·linux·jvm
mounter6251 小时前
重构 Kexec Handover:Linux 内核如何将 KHO 打造为无状态、可重入的跨重启交接基石
linux·内存管理·linux kernel·kernel·liveupdate
MartinYeung52 小时前
[论文学习]谄媚研究者驱动表演性错位:大语言模型“对齐伪装”成因的颠复性实证研究
人工智能·学习·语言模型
red_redemption2 小时前
自由学习记录(222)
学习
杨云龙UP2 小时前
Linux 下 MySQL 常用登录方式及 Socket 排查指南
linux·运维·数据库·mysql·登录·xtrabackup·主从复制
鱼很腾apoc2 小时前
【Linux】第13期 详解应用层协议HTTP+Cookie/Session+HTTPS
linux·服务器·c++·学习·http·https