当前位置: 首页 > news >正文

【Linux】线程同步与互斥(1)

1. 线程互斥

在多线程环境中,线程共享进程的地址空间和资源(如全局变量、堆内存、文件描述符)。当多个线程同时访问临界资源时,可能导致数据不一致或逻辑错误,因此需要通过 “互斥” 机制保证临界区的 “原子性” 访问。本节从核心概念入手,解析线程互斥的必要性与基础原理。

1-1 线程互斥的核心背景概念

要理解线程互斥,需先明确以下 5 个关键概念,它们共同构成了互斥机制的理论基础:

概念定义与作用
共享资源多线程可共同访问的资源(如全局变量、堆内存、文件、硬件设备等)。
临界资源共享资源中需被保护的部分 —— 若多个线程同时操作,会导致数据错乱或逻辑错误。
临界区线程内部访问临界资源的代码段(如修改全局变量的 3 行代码)。
互斥一种同步机制:任何时刻仅允许一个线程进入临界区,确保临界资源的独占访问。
原子性操作的 “不可分割性”—— 要么完整执行,要么完全不执行,不会被调度机制打断。

1. 共享资源 vs 临界资源

  • 共享资源是 “可被多线程访问的资源”,范围更广;
  • 临界资源是 “共享资源中需保护的子集”,是互斥机制的核心保护对象。示例:
    • 进程的全局变量int count = 0是共享资源,若多个线程执行count++,则count属于临界资源(并发修改会导致数据错误);
    • 进程的代码段是共享资源,但仅读取不修改时,无需保护,不属于临界资源。

2. 临界区:互斥保护的 “代码范围”

临界区是线程中 “直接操作临界资源的代码”,需满足两个条件:

  • 仅包含 “必须独占执行的代码”(范围越小越好,减少线程阻塞时间);
  • 多线程的临界区需针对同一临界资源(否则无需互斥)。

示例:若线程 A 和线程 B 都执行count++,则count++对应的汇编代码(加载、加 1、存储)是临界区,需用互斥机制保护。

3. 互斥:解决 “并发冲突” 的核心机制

多线程并发访问临界资源时,会出现 “竞态条件(Race Condition)”—— 最终结果依赖线程的执行顺序,导致数据不一致。

竞态条件示例count++的问题):count++看似是 1 行代码,实则对应 3 条汇编指令:

  1. load:将内存中的count值加载到 CPU 寄存器;
  2. add:寄存器中的值加 1;
  3. store:将寄存器中的值写回内存。

若线程 A 和线程 B 同时执行count++,可能出现以下执行顺序:

  • 线程 A 执行load(寄存器 A=0);
  • 线程 B 执行load(寄存器 B=0);
  • 线程 A 执行add(寄存器 A=1)→ store(内存count=1);
  • 线程 B 执行add(寄存器 B=1)→ store(内存count=1)。

最终count仅从 0 变为 1,而非预期的 2—— 这就是 “竞态条件”,需通过互斥解决:确保线程 A 执行完 3 条指令(完整进入并退出临界区)后,线程 B 才能进入临界区。

4. 原子性:互斥的 “底层保障”

原子性是互斥机制的核心特性 —— 只有当临界区的操作具备原子性时,才能避免竞态条件。

  • 原子操作的本质:不会被调度机制打断(CPU 不会在原子操作执行过程中切换线程);
  • 硬件支持:CPU 提供原子指令(如xchgcmpxchg),操作系统基于这些指令实现互斥锁(如pthread_mutex_t),最终让临界区的访问具备原子性。

示例:通过互斥锁保护count++后,线程 A 进入临界区执行count++时,线程 B 会被阻塞,直到线程 A 释放锁 —— 此时count++的 3 条指令对线程 B 而言,相当于 “不可分割的原子操作”。

总结

  • 线程互斥的核心目标:解决多线程对临界资源的并发冲突,保证数据一致性。
  • 关键逻辑链:多线程共享临界资源 → 需保护临界区 → 通过互斥机制确保临界区访问的原子性 → 避免竞态条件。
  • 后续重点:基于 POSIX 线程库的pthread_mutex_t(互斥锁),实现临界区的原子性访问,这是线程互斥的工程实现核心。

2. 线程同步:条件变量、信号量与生产者消费者模型

线程同步是在保证数据安全(互斥) 的基础上,进一步控制线程的执行顺序,避免线程 “无效等待” 或 “饥饿”,实现线程间的协同工作。核心工具包括条件变量信号量,而生产者消费者模型是同步机制的经典应用场景。

2-1 同步核心概念与竞态条件

1. 同步的定义

同步(Synchronization):在多线程环境中,通过特定机制(如条件变量、信号量),让线程按照预期顺序访问临界资源,既保证数据安全,又避免线程因 “条件不满足” 而空等(如消费者等待队列非空、生产者等待队列非满)。

2. 竞态条件(Race Condition)

  • 本质:线程执行结果依赖于 “线程调度顺序”,导致数据不一致或逻辑错误。
  • 产生场景:多线程并发访问临界资源,且操作不具备原子性。
  • 解决思路
    1. 用互斥锁(pthread_mutex_t)保证临界区原子性(解决 “数据安全”);
    2. 用同步机制(条件变量 / 信号量)控制线程执行顺序(解决 “顺序协同”)。

2-2 条件变量(pthread_cond_t

条件变量是线程间 “基于条件的同步工具”:当线程的执行依赖于某个 “条件”(如队列非空、队列非满)时,可通过条件变量让线程在条件不满足时阻塞,条件满足时被唤醒。

2.2.1 条件变量的核心函数

函数原型功能关键说明
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)初始化条件变量attrNULL表示默认属性,通常动态初始化或用宏PTHREAD_COND_INITIALIZER静态初始化
int pthread_cond_destroy(pthread_cond_t *cond)销毁条件变量仅初始化过的条件变量可销毁,销毁后不可再使用
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)等待条件满足1. 原子操作:阻塞线程 + 释放互斥锁;2. 被唤醒后:自动重新竞争互斥锁,成功后返回;3. 必须与互斥锁配合使用
int pthread_cond_signal(pthread_cond_t *cond)唤醒一个等待线程从等待队列中随机唤醒一个线程,避免 “惊群效应”
int pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有等待线程适用于 “条件满足时所有线程都需响应” 的场景(如广播 “队列已清空”)

2.2.2 为什么pthread_cond_wait需要互斥锁?

条件变量的核心是 “基于共享条件的同步”,而 “条件” 本质是共享数据(如队列的空 / 满状态),必须用互斥锁保护,否则会出现以下问题:

  1. 条件判断与阻塞的原子性问题:若先解锁再阻塞(错误示例如下),在 “解锁” 到 “阻塞” 的间隙,其他线程可能修改条件并发送信号,导致当前线程错过信号,永久阻塞。
    // 错误示例:解锁与阻塞非原子
    pthread_mutex_unlock(&mutex); 
    // 间隙:其他线程修改条件并发送信号,当前线程未收到
    pthread_cond_wait(&cond, &mutex); 
    
  2. pthread_cond_wait的原子性保障:函数内部会先检查条件,若不满足则原子执行 “释放锁 + 阻塞线程”,避免上述间隙;被唤醒后,会自动重新竞争锁,确保后续访问条件时的线程安全。

2.2.3 条件变量使用规范(避坑关键)

1. 等待条件:用while而非if

条件变量存在 “伪唤醒”(Spurious Wakeup)—— 线程被唤醒但条件仍不满足(如系统调度误唤醒)。因此必须用while循环重新检查条件:

pthread_mutex_lock(&mutex);
// 用while循环:伪唤醒后重新检查条件
while (条件不满足) { pthread_cond_wait(&cond, &mutex); 
}
// 条件满足:执行临界区操作
pthread_mutex_unlock(&mutex);
2. 发送信号:先修改条件,再唤醒线程

唤醒前必须确保 “条件已满足”,且修改条件的操作需在互斥锁保护下:

pthread_mutex_lock(&mutex);
// 1. 先修改条件(如队列添加元素,使“队列非空”条件成立)
修改共享条件; 
// 2. 再唤醒等待线程
pthread_cond_signal(&cond); 
pthread_mutex_unlock(&mutex);

2.2.4 条件变量封装

Cond.hpp

// 条件变量的封装#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"using namespace MutexModule;namespace CondModule
{class Cond{public:Cond(){pthread_cond_init(&_cond, nullptr);}void Wait(Mutex &mutex){int n = pthread_cond_wait(&_cond, mutex.Get());if (n != 0){std::cout << "pthread_cond_wait error: " << n << std::endl;}}void Signal(){int n = pthread_cond_signal(&_cond);if (n != 0){std::cout << "pthread_cond_signal error: " << n << std::endl;}}void Broadcast(){int n = pthread_cond_broadcast(&_cond);if (n != 0){std::cout << "pthread_cond_broadcast error: " << n << std::endl;}}~Cond(){pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}

运行结果:主线程每 1 秒唤醒一次,两个线程交替或同时输出 “活动” 信息。

2-3 POSIX 信号量(sem_t

信号量是一种 “计数器同步工具”,本质是通过原子操作控制共享资源的访问次数,可用于线程间或进程间同步。核心逻辑是 “P 操作(申请资源,计数器 - 1)” 和 “V 操作(释放资源,计数器 + 1)”。

2.3.1 信号量核心函数

函数原型功能关键说明
int sem_init(sem_t *sem, int pshared, unsigned int value)初始化信号量1. pshared:0 = 线程间共享,非 0 = 进程间共享;2. value:信号量初始值(资源总数)
int sem_destroy(sem_t *sem)销毁信号量仅初始化过的信号量可销毁
int sem_wait(sem_t *sem)P 操作(申请资源)1. 若信号量值 > 0:值 - 1,立即返回;2. 若值 = 0:阻塞线程,直到值 > 0
int sem_post(sem_t *sem)V 操作(释放资源)信号量值 + 1,若有线程阻塞则唤醒一个

 2.3.2 信号量的封装

Sem.hpp

#pragma once#include <iostream>
#include <pthread.h>
#include <semaphore.h>namespace SemMoudle
{const int defaultvalue = 1;class Sem{public:Sem(unsigned int value = defaultvalue){sem_init(&_sem, 0, value);}void P() // 等待{int n = sem_wait(&_sem);if (n != 0){std::cout << "sem_wait error" << std::endl; // 出错处理}}void V() // 通知{int n = sem_post(&_sem);if (n != 0){std::cout << "sem_post error" << std::endl; // 出错处理}}~Sem(){sem_destroy(&_sem);}private:sem_t _sem;};
}

2.3.3 信号量与互斥锁的区别

特性互斥锁(pthread_mutex_t信号量(sem_t
用途保证临界区原子性(互斥)控制资源访问次数(同步 + 互斥)
所有权排他性:同一时间仅一个线程持有无所有权:线程可释放其他线程申请的资源
初始值1(二值信号量)可自定义(如 N=5 表示 5 个资源)
适用场景单资源互斥访问(如全局变量)多资源同步(如环形队列的空 / 满控制)

2-4 生产者消费者模型(同步机制的经典应用)

生产者消费者模型是 “解耦生产者与消费者、平衡处理能力” 的设计模式,核心是通过阻塞队列(缓冲区)实现两者的异步通信。

2.4.1 模型核心:321 原则

  • 3 种关系
    1. 生产者与生产者:互斥(避免同时修改队列导致数据错乱);
    2. 消费者与消费者:互斥(避免同时读取队列导致数据重复);
    3. 生产者与消费者:同步(生产者等待队列非满,消费者等待队列非空)+ 互斥(访问队列时独占)。
  • 2 种角色:生产者(生成数据并放入队列)、消费者(从队列取出数据并处理)。
  • 1 个缓冲区:阻塞队列(核心组件,实现解耦)。

2.4.2 模型优点

  1. 解耦:生产者与消费者无需知道对方存在,仅通过队列交互;
  2. 支持并发:生产者和消费者可独立并发执行,提高系统吞吐量;
  3. 支持忙闲不均:队列可缓冲 “生产者快、消费者慢” 或反之的情况,避免一方等待另一方。

2.4.3 基于条件变量的阻塞队列(单 / 多生产消费)

通过条件变量控制队列的 “空 / 满” 条件,互斥锁保护队列访问,实现阻塞队列(BlockQueue):

// 阻塞队列的实现
#ifndef MUTEX_HPP
#define MUTEX_HPP#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <queue>
#include "Mutex.hpp"
#include "Cond.hpp"// 生产者放数据进队列,消费者从队列取数据
// 当队列满了就要生产者进行等待,当队列空了就要消费者进行等待
// 一个队列,生产消费者数据交互的媒介
// 一把锁,为了让生产者放数据和消费者取数据的动作是原子的
// 两个条件变量, 为了使消费者和生产者在不同的情况下进行等待
using namespace MutexModule;
using namespace CondModule;
const int defaultcap = 5;template <typename T>
class BlockQueue
{
private:bool IsFull(){return _q.size() >= _cap;}bool IsEmpty(){return _q.empty();}public:BlockQueue(const int &cap = defaultcap): _cap(cap),_csleep_num(0),_psleep_num(0){}void Equeue(const T &in){LockGuard lockguard(_mutex);while (IsFull()){// 重点1. 挂起线程之前,要先释放锁// 重点2. 当线程被唤醒的时候,默认就在临界区内唤醒!//        要从pthread_con_wait成功返回,需要线程重新申请_mutex锁// 重点3. 如果被唤醒,但是申请锁失败,就会在锁上阻塞等待!_psleep_num++;_full_cond.Wait(_mutex);_psleep_num--;}_q.push(in);// 有数据了唤醒消费者// 唤醒操作是放在解锁前还是解锁后???都可以if(_csleep_num > 0){_empty_cond.Signal();std::cout << "wake up consumer..." << std::endl;}}T Pop(){LockGuard lockguard(_mutex);while (IsEmpty()){_csleep_num++;_empty_cond.Wait(_mutex);_csleep_num--;}T data = _q.front();_q.pop();// 有空间了唤醒生产者if(_psleep_num > 0){_full_cond.Signal();std::cout << "wake up producer..." << std::endl;}return data;}~BlockQueue(){}private:std::queue<T> _q;int _cap;Mutex _mutex;Cond _full_cond;Cond _empty_cond;int _csleep_num; // 消费者休眠个数int _psleep_num; // 生产者休眠个数
};#endif // MUTEX_HPP

2.4.4 基于信号量的环形队列(多生产消费)

环形队列用数组模拟,通过信号量控制 “空闲空间数”(_room_sem)和 “数据数”(_data_sem),互斥锁保护生产者 / 消费者内部的并发:

// POSIX信号量实现环形队列
// 规则怪谈:
// 1. 队列为空,生产者先行
// 2. 队列为满,消费者先行
// 3. 生产者不能把消费者套一个圈以上
// 4. 消费者不能超过生产者#pragma once
#include "Sem.hpp"
#include <vector>
#include "Mutex.hpp"using namespace SemMoudle;
using namespace MutexModule;template <typename T>
class RingQueue
{
public:RingQueue(): _cap(5),_blank_sem(_cap),_p_step(0),_c_step(0),_data_sem(_cap){_q.resize(_cap);};void Equeue(const T &in){// 先把信号量瓜分再申请锁// 申请空位置信号量_blank_sem.P();LockGuard mutexguard(_pmutex);_q[_p_step] = in;_p_step = (_p_step + 1) % _cap;// 通知数据信号量_data_sem.V();}void Pop(T *out){// 申请数据信号量_data_sem.P();LockGuard mutexguard(_cmutex);*out = _q[_c_step];_c_step = (_c_step + 1) % _cap;// 通知空位置信号量_blank_sem.V();}~RingQueue() {};private:std::vector<T> _q; // 数组模拟环形队列int _cap;Sem _blank_sem; // 空位置信号量,供生产者竞争int _p_step; // 生产者下标:标记下一个要写入的位置Sem _data_sem; // 数据信号量,供消费者竞争int _c_step; // 消费者下标:标记下一个要读取的位置Mutex _pmutex;Mutex _cmutex;
};

总结

  • 条件变量:基于 “条件判断” 的同步工具,需与互斥锁配合,解决 “线程等待特定条件” 的问题,核心是pthread_cond_wait的原子性。
  • 信号量:基于 “计数器” 的同步工具,可同时实现互斥与同步,适用于 “多资源访问控制”。
  • 生产者消费者模型:同步机制的经典应用,通过阻塞队列解耦生产者与消费者,平衡处理能力,核心是解决 “3 种关系” 的同步与互斥。
  •  如果资源可以被瓜分就考虑用信号量,如果是整体的就考虑用互斥锁
http://www.dtcms.com/a/483013.html

相关文章:

  • 网站开发英语英语义乌网八方资源家1688网商网
  • 基于单片机的PID调节脉动真空灭菌器上位机远程监控设计
  • 汕头网站关键词优化教程资源分享网站怎么做
  • STM32H7xx 运行 LWIP 时的 MPU 配置介绍 LAT1510
  • 能动框架战场:如何摆脱供应商锁定并在下次AI战争中生存
  • 免费试用网站空间人人开发接单官网
  • 视联网技术产业观察与分析:视频隐私与安全防护
  • 南通网站建设祥云深圳罗湖网站设计公司
  • 基于蚁群算法优化BP神经网络的实现方法
  • 《Effective Java》第10条:覆盖 equals 时请遵守通用规定
  • 广东广州快速网站制作平台鄂州网站建设哪家好
  • 安卓android自动化测试-uiautomator/uiautomator2
  • 天津 网站设计公司门户网站制作定做
  • React组件复用导致的闪烁问题及通用解决方案
  • Java EE开发技术(Servlet整合JDBC银行管理系统-上)
  • 深入理解string底层:手写高效字符串类
  • 做国际网站有用吗基础建设图片
  • 启动hbase后,hbmaster总是挂
  • 自助网站建设开发流程步骤西安活动策划执行公司
  • 计算机系统---CPU的进程与线程处理
  • cv_bridge和openCV不兼容问题
  • json转excel python pd
  • 上海网站建设排名公司哪家好天蝎网站建设公司
  • 进入网络管理的网站不想用原来的网站模板了就用小偷工具采集了一个可是怎么替换
  • 西安注册公司在哪个网站系统哈尔滨模板网站
  • android 开机启动 无线调试
  • Polaris Officev9.9.12全功能解锁版
  • 云信im在Android的使用
  • 王道数据结构应用题强化表3.1.1-3.1.6
  • JDK 1.8 自动化脚本安装方案