云计算百科
云计算领域专业知识百科平台

Linux 线程互斥

线程同步与互斥总结

本章主线是“先互斥,再同步”:

部分解决的问题手段
1. 线程互斥(本节) 同一时刻只能有一个执行流进入临界区 互斥量 pthread_mutex_t
2. 线程同步(后续) 按特定顺序访问,避免饥饿 条件变量、信号量

课件本节四个目标:1-1 背景概念 → 1-2 互斥量 mutex → 1-3 实现原理 → 1-4 封装。

1. 五个背景概念

概念含义
共享资源 多个线程都能访问的资源
临界资源 多线程执行流中被保护的共享资源
临界区 每个线程内部访问临界资源的那段代码
互斥 任何时刻保证有且只有一个执行流进入临界区、访问临界资源,通常对临界资源起保护作用
原子性 不会被任何调度机制打断的操作;只有两态:要么完成,要么未完成(如何实现留待 1-3 讨论)

2. 为什么需要互斥:售票系统反例

ticket = 100 全局变量,4 个线程各自 while(1):

while (1) {
if (ticket > 0) {
usleep(1000); // 模拟漫长的业务过程
printf("%s sells ticket:%d\\n", id, ticket);
ticket—; // 非原子
} else break;
}

一次真实运行结果:thread 4 卖到 1,thread 2 卖 0,thread 1 卖 -1,thread 3 卖 -2 —— 既重复超卖又卖出负数票。

原因逐条拆解如下:

  • if 语句判断条件为真之后,代码可以并发的切换到其他线程(判断与修改之间留出了空档);
  • usleep 模拟的漫长业务过程中,可能有很多线程已经“看到 ticket>0”并进入该代码段;
  • –ticket 本身不是原子操作,objdump -d 可见它对应三条汇编:
  • mov 0x2004e3(%rip),%eax # load :从内存读到寄存器
    sub $0x1,%eax # update :寄存器内 -1
    mov %eax,0x2004da(%rip) # store :写回内存

    注意:

    • 代码必须要有互斥行为:进入临界区执行时不允许其他线程进入;
    • 多个线程同时要求执行临界区、而临界区无人执行时,只能允许一个线程进入;
    • 线程不在临界区中执行时,不能阻止其他线程进入。

    本质结论:需要一把锁,Linux 上它就是互斥量。

    3. 互斥量接口

    初始化

    // 方法1 静态分配(全局/静态)
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    // 方法2 动态分配
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);
    // mutex:要初始化的互斥量 attr:NULL

    销毁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    三条注意:① 用 PTHREAD_MUTEX_INITIALIZER 初始化的不需要销毁;② 不要销毁一个已经加锁的互斥量;③ 已销毁的互斥量要确保后续没有线程再尝试加锁。

    加锁 / 解锁

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    // 返回值:成功返回 0,失败返回错误号

    pthread_mutex_lock 的两种情形:

    • 互斥量未锁 → 直接锁定并返回成功;
    • 已被其他线程锁定,或同时有其他线程申请但没竞争到 → 调用陷入阻塞(执行流被挂起),等待互斥量解锁。

    改造后的售票代码

    pthread_mutex_t mutex;
    void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
    pthread_mutex_lock(&mutex);
    if (ticket > 0) {
    usleep(1000);
    printf("%s sells ticket:%d\\n", id, ticket);
    ticket—;
    pthread_mutex_unlock(&mutex);
    } else {
    pthread_mutex_unlock(&mutex); // 注意:break 之前也必须解锁
    break;
    }
    }
    return NULL;
    }
    int main(void) {
    pthread_mutex_init(&mutex, NULL);
    /* create t1..t4, join t1..t4 */
    pthread_mutex_destroy(&mutex);
    }

    锁必须覆盖 if 判断 + ticket– 整个逻辑,只锁 ticket– 依然会超卖。

    4. 互斥量实现原理

    • i++ / ++i 都不是原子的,直接共享会带来数据一致性问题;
    • 为实现互斥锁,大多数体系结构都提供 swap 或 exchange 指令,作用是把寄存器与内存单元的数据相交换;
    • 因为只有一条指令,保证了原子性:即使多处理器平台,访问内存的总线周期也有先后,一个处理器执行交换指令时,另一个处理器的交换指令只能等总线周期。

    lock: ; 加锁
    mov $0, %al
    xchg %al, mutex ; 原子交换
    if (%al > 0) return 0 ; 原来 mutex==1,交换成功拿到锁
    else { 挂起等待; goto lock; }

    unlock: ; 解锁
    mov $1, %al
    xchg %al, mutex
    if (%al > 0) { 唤醒等待该 mutex 的线程; return 0; }
    return 0;

    要点:所谓“加锁”就是一次原子交换;没换到(拿到 0)就说明锁被别人持着,只能挂起等待 —— 这就是 mutex 的底层机制。

    5. 互斥量的 C++ 封装(1-4)

    Lock.hpp,命名空间 LockModule,两个类:

    ① Mutex(可独立使用的锁本体)

    class Mutex {
    public:
    Mutex(const Mutex &) = delete; // 禁止拷贝
    const Mutex &operator=(const Mutex &) = delete; // 禁止赋值
    Mutex() { pthread_mutex_init(&_mutex, nullptr); }
    void Lock() { pthread_mutex_lock(&_mutex); }
    void Unlock() { pthread_mutex_unlock(&_mutex); }
    pthread_mutex_t *GetMutexOriginal() { return &_mutex; } // 需要原生接口时用
    ~Mutex() { pthread_mutex_destroy(&_mutex); }
    private:
    pthread_mutex_t _mutex;
    };

    ② LockGuard(RAII 风格的锁管理)

    class LockGuard {
    public:
    LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
    ~LockGuard() { _mutex.Unlock(); }
    private:
    Mutex &_mutex;
    };

    用起来业务代码变得极简,且任何出口都不会漏解锁:

    Mutex mutex;
    while (1) {
    LockGuard lockguard(mutex); // 作用域开始 → 加锁
    if (ticket > 0) { usleep(1000); printf(...); ticket—; }
    else break; // 作用域结束/break → 析构自动解锁
    }

    RAII 风格的互斥锁 C++11 已内建,对应 std::mutex + std::lock_guard<std::mutex>;本节只是自行封装以便后面(生产者消费者、线程池)直接使用。

    6. 要点速记与易错点

  • 锁的范围 = 临界区:判断条件与修改共享变量必须同时在锁内,不能只锁 –。
  • 每个出口都要解锁:正常分支、else 分支、break、异常路径 —— 用 LockGuard 一次性解决。
  • 静态初始化不需要 destroy;不要销毁加锁中的/仍会被使用的锁。
  • pthread_mutex_* 失败是返回错误号,不设置 errno。
  • 不要持锁做耗时操作:例子把 usleep/printf 放在锁内,等于把并发重新压回串行(能正确,但吞吐变差)。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux 线程互斥
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!