【操作系统学习日记】并发编程中的竞态条件与同步机制:互斥锁与信号量

在并发编程中,当多个线程同时访问共享数据且执行顺序影响结果时,就会产生竞态条件(Race Condition)。这种不确定性会导致共享数据的最终值不一致,甚至损坏。以下通过经典案例说明:

场景:两个线程对初始值 count = 5 执行操作:

  • 线程 A:count \gets count + 1
  • 线程 B:count \gets count - 1

问题 :若底层指令交错执行(如 LOAD → ADD → STORELOAD → SUB → STORE 混合),结果可能是 4、5 或 6,而非逻辑正确的 5。

为解决此问题,需通过同步机制保护临界区(Critical Section)------访问共享资源的代码段。其标准流程包括:

  1. 进入区:请求进入权限
  2. 临界区:操作共享数据
  3. 退出区:释放权限
  4. 剩余区:执行非共享代码

有效方案需满足三要求:

  • 互斥:同一时刻仅一个线程进入临界区
  • 进步:系统不会无限推迟进入决策
  • 有限等待:等待时间存在上限

一、互斥锁(Mutex Locks)

互斥锁是最基础的同步工具,核心机制可类比为唯一钥匙

python 复制代码
def acquire():
    while not available:  # 自旋等待
        pass
    available = False     # 取走钥匙

def release():
    available = True      # 归还钥匙

核心机制:唯一的一把钥匙

你可以把互斥锁想象成进入一间更衣室(临界区)的唯一钥匙

  • 获取锁 (acquire()):程序员在进入临界区前,必须调用这个函数来请求"钥匙"。
  • 如果锁是空闲的(available 为 true),线程就拿走钥匙,并将状态改为不可用(available 为 false),然后进入临界区。
  • 如果锁已经被别人拿走了,想要进入的线程就会被阻塞,直到锁被释放。
  • 释放锁 (release()):线程执行完任务离开临界区时,必须调用这个函数把钥匙还回去(available 重新设为 true)。
核心特性:
  1. 原子操作acquire()release() 通过硬件指令(如 Test-and-Set)实现不可中断性
  2. 等待策略
    • 自旋锁 :线程会像陀螺一样在原地不停地循环检查锁是否可用。
      • 优点:不需要进行复杂的"上下文切换",如果锁很快就会被释放,这种方式效率极高。
      • 缺点:它会持续占用 CPU 资源,浪费计算周期,这种现象被称为"忙等" (Busy Waiting)。
    • 睡眠锁 :如果锁不可用,操作系统会让该线程进入"睡眠"状态(挂起),并把它放入锁的等待队列中。适合长临界区
      • 优点:释放了 CPU 资源,让别的任务去运行。
      • 缺点:当锁可用时,唤醒线程和切换上下文需要消耗较多的时间。
应用示例(POSIX):
c 复制代码
pthread_mutex_t lock;
pthread_mutex_lock(&lock);   // 进入临界区
/* 操作共享数据 */
pthread_mutex_unlock(&lock); // 离开临界区

二、信号量(Semaphores)

信号量是功能更强的同步工具,可视为资源计数器 S,支持两种原子操作:

python 复制代码
def P(S):                   # wait()
    S -= 1
    if S < 0:               
        block()             # 资源不足则阻塞

def V(S):                   # signal()
    S += 1
    if S <= 0:              
        wakeup()            # 唤醒等待进程
类型与用途:
  1. 二元信号量 :S \in {0,1}
    • 功能等价于互斥锁,实现互斥访问
  2. 计数信号量 :S \geq 0
    • 管理多实例资源(如 N 个数据库连接)
等待机制优化:
  • 阻塞替代忙等:进程进入等待队列,释放 CPU
  • 唤醒策略:V 操作自动唤醒等待进程
风险防范:
  • 操作顺序:P 必须在 V 前执行,否则破坏互斥
  • 资源泄漏:忘记 V 操作将导致死锁

信号量(详细介绍)

一个"共享自习室的管理系统"。

信号量本质上是一个整数变量 S,代表可用资源的数量。

  • 计数值的含义:它告诉系统还有多少个"位置"可以使用。
  • 例子:如果自习室有 5 个空位,信号量 S 的初始值就是 5。

P 和 V

为了保证数据安全,不能直接修改这个数字,只能通过两个"原子操作"(即不可分割、不会被中途干扰的操作)来访问它:

  • P 操作(也叫 wait())------"申请资源"逻辑:想进自习室。会先检查有没有位子。如果有(S>0),就把 S 减 1 然后坐下开始学习;如果没位子了(S≤0),就必须在门口排队等候。
  • 口诀:有位子就占,没位子就等。
  • V 操作(也叫 signal())------"释放资源"逻辑:学完了要离开。把 S 加 1。如果此时门口有人在排队(在某些实现中,S 加 1 后若仍 ≤0),系统会立刻从等待队列里叫醒一个人,让他进去坐下。
  • 口诀:学完腾位子,叫醒后来人。

信号量的两种"变身"

  • 二元信号量 (Binary Semaphore) :计数值只能是 0 或 1。它就像一把唯一的钥匙,作用和互斥锁 (Mutex) 几乎一样,保证同一时间只有一个进程能操作共享数据。
  • 计数信号量 (Counting Semaphore) :计数值可以是任意非负整数。它专门用来管理有多个实例的资源,比如 3 台打印机或 10 个网络连接。

常见的使用风险

  • 顺序写反:先执行了 V 再执行 P,可能会导致多个进程同时闯入临界区,造成数据损坏。
  • 忘记释放 :占着资源(执行了 P)却忘了归还(执行 V),排队的人将永远等待下去,引发死锁 (Deadlock)

简单总结: 信号量就是一个带排队机制的智能计数器,它能确保有限的资源被有序、安全地分配给多个竞争者。


总结

机制 适用场景 核心优势 潜在缺陷
互斥锁 单一资源互斥访问 实现简单、低开销 不支持资源池管理
信号量 多资源池或复杂同步逻辑 灵活控制资源数量 操作错误易引发死锁

通过合理选用互斥锁或信号量,可有效消除竞态条件,确保并发程序的正确性与稳定性。

相关推荐
Irene19912 小时前
JavaScript脚本加载的两种方式:defer/async 的区别
前端·javascript·php
Predestination王瀞潞2 小时前
Base Tools-Associate-Fifth:re库详解
数据库·mysql
wanhengidc2 小时前
云手机与模拟器的关系
大数据·运维·服务器·分布式·智能手机
爱喝白开水a2 小时前
春节后普通程序员如何“丝滑”跨行AI:不啃算法,也能拿走AI
java·人工智能·算法·spring·ai·前端框架·大模型
Ricky_Theseus2 小时前
SQL Server2008 select语句基本语法
数据库·sql
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于Java的运动场地预约系统为例,包含答辩的问题和答案
java·开发语言
蜜獾云2 小时前
Spring Cloud Hystrix 详细示-元一软件
java·spring cloud·hystrix
烛之武2 小时前
SpringBoot 实战篇
java·spring boot·后端
lclcooky2 小时前
Spring 核心技术解析【纯干货版】- XII:Spring 数据访问模块 Spring-R2dbc 模块精讲
java·后端·spring