Linux线程同步与互斥(一):互斥量与锁的理解

张开发
2026/4/20 9:51:24 15 分钟阅读

分享文章

Linux线程同步与互斥(一):互斥量与锁的理解
在多线程编程中线程之间的协作和数据共享是常态但也是问题的根源。问题多个线程同时卖票结果卖出了负数或者某个线程“饿死”一直得不到执行这些问题都源于资源共享和执行顺序的不可控。一、进程线程间互斥相关背景概念临界资源Critical Resource多个线程共享的资源如全局变量、文件、设备等称为临界资源。多个线程同时访问它可能会导致数据不一致。被保护起来的共享资源 -- 临界资源临界区Critical Section访问临界资源的代码段称为临界区。为了保证数据安全我们需要保证同一时刻只有一个线程进入临界区。互斥Mutex互斥是指同一时刻只允许一个线程进入临界区。互斥是对临界资源的保护机制。原子性Atomic一个操作如果不可被中断要么完全执行要么完全不执行就称为原子操作。例如ticket--在C语言中看起来是一条语句但在CPU层面是多条指令不是原子的。二、互斥量mutex大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况 变量归属当个线程其他线程无法获得这种变量但有的时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互多个线程并发的操作共享变量会带来一些问题。2.1 现象/解决无法获得争取结果的原因if 语句判断条件为真以后 代码可以并发的切换到其他线程usleep 这个模拟漫长业务的过程在这个漫长的业务过程中可能会有很多线程进入该代码段--ticket操作本身就不是一个原子操作// 操作共享变量会有问题的售票系统代码 #include stdio.h #include stdlib.h #include string.h #include unistd.h #include pthread.h int ticket 1000; void *route(void *arg) { char *id (char *)arg; while (1) { // 1. 判断 if (ticket 0) { // 模拟抢票花的时间 usleep(1000); // 2.抢到了票 printf(%s sells ticket:%d\n, id, ticket); // 3.票数-- ticket--; } else { break; } } return nullptr; } int main(void) { pthread_t t1, t2, t3, t4; pthread_create(t1, NULL, route, (char *)thread 1); pthread_create(t2, NULL, route, (char *)thread 2); pthread_create(t3, NULL, route, (char *)thread 3); pthread_create(t4, NULL, route, (char *)thread 4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); }通过加锁来解决这个问题 :#include pthread.h // 1. 定义并初始化互斥锁 pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; // 2. 加锁阻塞直到获取锁 int pthread_mutex_lock(pthread_mutex_t *mutex); // 3. 解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);2.2 为什么会减到负数核心问题不是ticket--的问题是if (ticket 0)和ticket--之间的时间窗口ticket -- : 不是原子任意操作之后线程会被切换 导致数据不一致的问题2.2.1 票数为1时场景介绍2.2.2 根本原因判断和修改是两个独立的操作if (ticket 0) { // ←── 操作1判断只读不修改内存 // 中间可能有任意长时间的延迟任意多次线程切换 ticket--; // ←── 操作2修改此时判断结果可能已经失效 }--操作并不是原子操作而是对应三条汇编指令load :将共享变量 ticket 从内存加载到寄存器中upload :更新寄存器里面的值执行 -1 操作store :将新值 ,从寄存器里写回共享变量 ticket 的内存地址注意CPU能够识别指令的长度关键洞察if (ticket 0)只是读取内存值不会改变内存中的 ticket四个线程读取时ticket 都是 1所以四个都判断为真等到它们依次执行ticket--时已经没有保护了2.2.3汇编级别的拆解更糟的情况线程A在if判断后、ticket--前被切走其他线程修改了 ticket但A恢复后仍然执行减操作2.3 线程切换的关键时机线程切换发生在什么时候时间片耗尽— 操作系统调度阻塞式 I/O— 如printf、read等主动休眠—sleep、usleep等陷入内核— 系统调用时什么时候选择新的从内核态返回到用户态的时候进行检查重要机制操作系统在从内核态返回用户态时会检查是否需要调度时间片是否用完、是否有更高优先级线程等决定是否进行线程切换。usleep让线程主动放弃 CPU增加了线程切换的概率使得ticket--的三条汇编指令之间更容易被中断从而更容易观察到数据不一致。原子操作 vs 互斥锁原子操作硬件级别的支持如 x86 的LOCK前缀适用于简单操作计数器增减互斥锁操作系统级别的同步机制适用于复杂的临界区保护2.4 互斥锁Mutex互斥量mutex是一种锁机制用于保护临界区确保同一时刻只有一个线程进入。要解决问题必须做到临界区同时只能有一个线程进入线程进入后不被干扰退出后其他线程才能进入Linux 提供的解决方案互斥量 mutex一把锁。、1. 竞争申请锁 多线程都先看到锁锁本身就是临界资源申请锁的过程必须是原子的2.2.4.1 初始化互斥量注意用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁 程序运行结束会自动释放初始化互斥量有两种方法静态分配pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 静态初始化动态初始化int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);mutex : 要初始化的互斥量attr : NULL如果锁是空闲的当前线程获得锁如果锁被占用线程阻塞等待2.4.2 解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁唤醒等待的线程2.4.3 销毁锁int pthread_mutex_destroy(pthread_mutex_t *mutex);注意静态初始化的锁不用销毁不能销毁正在加锁的锁2.5 代码#include stdio.h #include iostream #include stdlib.h #include string.h #include unistd.h #include mutex #include pthread.h int ticket 1000; pthread_mutex_t glock PTHREAD_MUTEX_INITIALIZER; //全局的锁 class ThreadData { public: ThreadData(const std::string n, pthread_mutex_t lock) : name(n), lockp(lock) {}; ~ThreadData() {}; std::string name; pthread_mutex_t *lockp; }; //加锁尽量加锁的范围力度要比较细尽可能不要包含太多的非临界区的代码 void *route(void *arg) { ThreadData *td static_castThreadData*(arg); while (1) { //pthread_mutex_lock(td-lockp); pthread_mutex_lock(glock); cpp_lock.lock(); // 1. 判断 if (ticket 0) { // 模拟抢票花的时间 usleep(1000); // 2.抢到了票 printf(%s sells ticket:%d\n, td-name.c_str(), ticket); // 3.票数-- ticket--; pthread_mutex_unlock(glock); } else { pthread_mutex_unlock(glock); break; } } return nullptr; } int main(void) { pthread_mutex_t lock; pthread_mutex_init(lock,nullptr);//初始化锁 pthread_t t1, t2, t3, t4; ThreadData *td1 new ThreadData(thread 1, lock); pthread_create(t1, NULL, route, td1); ThreadData *td2 new ThreadData(thread 2, lock); pthread_create(t2, NULL, route, td2); ThreadData *td3 new ThreadData(thread 3, lock); pthread_create(t3, NULL, route, td3); ThreadData *td4 new ThreadData(thread 4, lock); pthread_create(t4, NULL, route, td4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); pthread_mutex_destroy(lock);//回收锁 return 0; }pthread_mutex_lock(mutex);尝试加锁如果锁已被占用当前线程会阻塞在这里。if (ticket 0)进入临界区后再次检查票数确保安全。printf(...)和ticket--这些操作现在都在锁的保护下不会被其他线程打断。pthread_mutex_unlock(mutex);释放锁让其他线程有机会进入临界区。⚠️ 注意usleep(1000)仍在锁内虽然是模拟耗时操作但会导致其他线程等待时间变长。实际开发中应尽量减少锁内耗时。使用C的mutex#include stdio.h #include iostream #include stdlib.h #include string.h #include unistd.h #include mutex #include pthread.h int ticket 1000; pthread_mutex_t glock PTHREAD_MUTEX_INITIALIZER; //全局的锁 std::mutex cpp_lock; class ThreadData { public: ThreadData(const std::string n, pthread_mutex_t lock) : name(n), lockp(lock) {}; ~ThreadData() {}; std::string name; pthread_mutex_t *lockp; }; //加锁尽量加锁的范围力度要比较细尽可能不要包含太多的非临界区的代码 void *route(void *arg) { ThreadData *td static_castThreadData*(arg); while (1) { //pthread_mutex_lock(td-lockp); //pthread_mutex_lock(glock); cpp_lock.lock(); // 1. 判断 if (ticket 0) { // 模拟抢票花的时间 usleep(1000); // 2.抢到了票 printf(%s sells ticket:%d\n, td-name.c_str(), ticket); // 3.票数-- ticket--; //pthread_mutex_unlock(glock); cpp_lock.unlock(); } else { //pthread_mutex_unlock(glock); cpp_lock.unlock(); break; } } return nullptr; } int main(void) { pthread_mutex_t lock; pthread_mutex_init(lock,nullptr);//初始化锁 pthread_t t1, t2, t3, t4; ThreadData *td1 new ThreadData(thread 1, lock); pthread_create(t1, NULL, route, td1); ThreadData *td2 new ThreadData(thread 2, lock); pthread_create(t2, NULL, route, td2); ThreadData *td3 new ThreadData(thread 3, lock); pthread_create(t3, NULL, route, td3); ThreadData *td4 new ThreadData(thread 4, lock); pthread_create(t4, NULL, route, td4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); pthread_mutex_destroy(lock);//回收锁 return 0; }对临界资源进行保护本质其实就是用锁来对临界区代码进行保护锁 --- 原子性三、互斥量实现原理探究经过上面的例子已经意识到单纯的 i 或者 i 都不是原子的 有可能会有数据一致性的问题为了实现互斥锁操作大多数体系结构都提供了swap 或 exchange指令该指令的作用是把寄存器和内存单元的数据相交换由于只有一条指令保证了原子性即使是多处理器平台访问内存的总线周期也有先后 一个处理器上的交换指令执行时另一个处理器的交换指令智能等待总线周期 。锁的实现靠CPU 原子指令如 xchg、swap整条指令不可打断核心交换指令是原子的保证同一时间只有一个线程能拿到锁。四、互斥量的封装4.1 Mutext.hpp#pragma once #include iostream #include pthread.h namespace MutextModule { class Mutex { public: Mutex() { pthread_mutex_init(_mutex, nullptr); } void Lock() { int n pthread_mutex_lock(_mutex); (void)n; } void Unlock() { int n pthread_mutex_unlock(_mutex); (void)n; } ~Mutex() { pthread_mutex_destroy(_mutex); } private: pthread_mutex_t _mutex; }; class LockGuard { public: LockGuard(Mutex mutex) : _mutex(mutex) { _mutex.Lock(); }; ~LockGuard() { _mutex.Unlock(); }; private: Mutex _mutex; }; }4.2 testMutex.cc#include stdio.h #include iostream #include stdlib.h #include string.h #include unistd.h #include pthread.h #include Mutex.hpp using namespace MutextModule; int ticket 1000; class ThreadData { public: ThreadData(const std::string n, Mutex lock) : name(n), lockp(lock) {}; ~ThreadData() {}; std::string name; Mutex *lockp; }; //加锁尽量加锁的范围力度要比较细尽可能不要包含太多的非临界区的代码 void *route(void *arg) { ThreadData *td static_castThreadData*(arg); while (1) { LockGuard guard(*td-lockp);//加锁完成 //td-lockp-Lock(); // 1. 判断 if (ticket 0) { // 模拟抢票花的时间 usleep(1000); // 2.抢到了票 printf(%s sells ticket:%d\n, td-name.c_str(), ticket); // 3.票数-- ticket--; //td-lockp-Unlock(); } else { //td-lockp-Unlock(); break; } } return nullptr; } int main(void) { Mutex lock; pthread_t t1, t2, t3, t4; ThreadData *td1 new ThreadData(thread 1, lock); pthread_create(t1, NULL, route, td1); ThreadData *td2 new ThreadData(thread 2, lock); pthread_create(t2, NULL, route, td2); ThreadData *td3 new ThreadData(thread 3, lock); pthread_create(t3, NULL, route, td3); ThreadData *td4 new ThreadData(thread 4, lock); pthread_create(t4, NULL, route, td4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); return 0; }4.3 MakefileTestMutex:TestMutex.cc g -o $ $^ -stdc11 -lpthread .PHONY:clean clean: rm -f TestMutex

更多文章