Linux学习笔记(十八)--mutex互斥量
在 Linux 中线程互斥用于防止多个线程同时访问共享资源从而避免数据竞争和不一致。背景概念临界资源定义一次仅允许一个线程或进程使用的共享资源。临界区定义每个线程内部访问临界资源的代码就叫做临界区互斥定义任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用原子性定义不会被任何调度机制打断的操作只有两态一个操作或多个操作要么全部执行且不被打断要么都不执行外界看到的是一个不可分割的完整操作。互斥量mutex大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个 线程其他线程无法获得这种变量。 但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之 间的交互。 多个线程并发的操作共享变量会带来一些问题。所以引入了互斥量的概念。概念互斥量 是一种用于实现线程间互斥的同步原语。它像一个“钥匙”同一时刻只允许一个线程持有。线程在进入临界区前必须先获取这把“钥匙”离开时归还其他想进入的线程必须等待。作用保护共享数据临界资源防止多个线程同时访问导致的数据竞争和不一致。互斥量的API申请锁成功才能往后执行不成功就要阻塞等待。纯互斥环境如果锁分配不够合理容易导致其他线程饥饿问题。让所有线程获取锁按照一定的顺序性来获取资源这个过程叫做同步。锁本身就是共享资源申请锁和释放锁这个过程本身就是原子性操作。在临界区中线程可以被切换在线程被切出去时是持有锁被切走的这样在线程不在期间没有任何其他线程能够访问临界区资源。初始化互斥量初始化互斥量有两种方法静态初始化pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;动态初始化运行时可设置属性int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);参数1mutex要初始化的互斥量 2attrNULL销毁互斥量int pthread_mutex_destroy(pthread_mutex_t *mutex);注意1使用 PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁2不要销毁一个已经加锁的互斥量3已经销毁的互斥量要确保后面不会有线程再尝试加锁加锁阻塞加锁如果锁已被占用则调用线程阻塞直到锁被释放。int pthread_mutex_lock(pthread_mutex_t *mutex);非阻塞尝试加锁立即返回成功返回0失败返回EBUSY。int pthread_mutex_trylock(pthread_mutex_t *mutex);带超时的加锁尝试在指定时间内获取锁。int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);加锁的本质用时间来换安全加锁表现线程对于临界区代码串行执行加锁原则尽量保证临界区的代码越少越好解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);调用pthread_mutex_lock时可能会遇到以下情况1互斥量处于未锁状态该函数会将互斥量锁定同时返回成功2发起函数调用时其他线程已经锁定互斥量或者存在其他线程同时申请互斥量但没有竞争到互斥量那么pthread_ lock调用会陷入阻塞(执行流被挂起)等待互斥量解锁。可重入与线程安全线程安全概念多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作 并且没有锁保护的情况下会出现该问题。重入概念同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们 称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重 入函数否则是不可重入函数。常见线程不安全情况1不保护共享变量的函数2函数状态随着被调用状态发生变化的函数3返回指向静态变量指针的函数4调用线程不安全函数的函数常见的线程安全情况1每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的2类或者接口对于线程来说都是原子操作3多个线程之间的切换不会导致该接口的执行结果存在二义性常见的可重入情况1调用了malloc/free函数因为malloc函数是用全局链表来管理堆的2调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构3可重入函数体内使用了静态的数据结构可重入与线程安全的联系1函数是可重入的那就是线程安全的2函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题3如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。可重入与线程安全的区别1可重入函数是线程安全函数的一种2线程安全不一定是可重入的而可重入函数则一定是线程安全的。3如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。常见锁概念死锁概念死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。必要条件死锁有四个必要条件分别是1互斥条件一个资源每次只能被一个执行流使用2请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放3不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺4循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系示例thread1持有A等Bthread2持有B等A → 死锁。pthread_mutex_t lockA PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lockB PTHREAD_MUTEX_INITIALIZER; void* thread1_func(void* arg) { pthread_mutex_lock(lockA); // 1. 获取A sleep(1); // 让thread2有机会获取B pthread_mutex_lock(lockB); // 3. 尝试获取B但被thread2持有 // 临界区AB pthread_mutex_unlock(lockB); pthread_mutex_unlock(lockA); return NULL; } void* thread2_func(void* arg) { pthread_mutex_lock(lockB); // 2. 获取B sleep(1); pthread_mutex_lock(lockA); // 4. 尝试获取A但被thread1持有 // 临界区BA pthread_mutex_unlock(lockA); pthread_mutex_unlock(lockB); return NULL; }避免死锁1破坏死锁的四个必要条件2加锁顺序一致3避免锁未释放的场景4资源一次性分配避免死锁的算法银行家算法概念首先让每个线程声明最大需求然后系统检查分配后是否处于安全状态避免进入不安全状态。假设系统有n 个进程P₁, P₂, ..., Pₙ和m 种资源类型R₁, R₂, ..., Rₘ。数据结构符号含义可用资源向量Available[m]每种资源的当前可用数量最大需求矩阵Max[n][m]每个进程对每种资源的最大需求分配矩阵Allocation[n][m]每个进程已获得的每种资源数量需求矩阵Need[n][m]每个进程还需要的每种资源数量重要关系Need[i][j] Max[i][j] - Allocation[i][j]示例资源类型A(10), B(5), C(7)进程P0, P1, P2, P3, P4Available [3, 3, 2] # 剩余可用 Allocation Max Need P0: [0, 1, 0] [7, 5, 3] [7, 4, 3] P1: [2, 0, 0] [3, 2, 2] [1, 2, 2] P2: [3, 0, 2] [9, 0, 2] [6, 0, 0] P3: [2, 1, 1] [2, 2, 2] [0, 1, 1] P4: [0, 0, 2] [4, 3, 3] [4, 3, 1]安全请求P1提出资源请求Request[1] [1, 0, 2]首先验证两个前提条件——一是检查请求是否不超过P1的需求上限Request ≤ Need[1]即[1,0,2] ≤ [1,2,2]验证通过二是检查请求是否不超过当前系统可用资源Request ≤ Available即[1,0,2] ≤ [3,3,2]验证通过。随后进行试探性资源分配系统可用资源Available更新为[3,3,2] - [1,0,2] [2,3,0]P1的已分配资源Allocation[1]更新为[2,0,0] [1,0,2] [3,0,2]P1的剩余需求Need[1]更新为[1,2,2] - [1,0,2] [0,2,0]。不安全请求P0进程提出资源请求Request[0] [0, 2, 0]算法同样先执行两步合法性检查——第一步验证请求是否不超过P0的需求上限Request ≤ Need[0]即[0,2,0] ≤ [7,4,3]验证通过第二步验证请求是否不超过当前系统可用资源Request ≤ Available即[0,2,0] ≤ [3,3,2]验证通过。接着进入试探分配阶段系统可用资源Available更新为[3,3,2] - [0,2,0] [3,1,2]P0的已分配资源Allocation[0]更新为[0,1,0] [0,2,0] [0,3,0]P0的剩余需求Need[0]更新为[7,4,3] - [0,2,0] [7,2,3]。之后进行安全检查发现此时所有进程的Need都无法被当前Available满足即不存在一个安全序列因此银行家算法判定该请求会导致系统进入不安全状态最终拒绝P0的请求。优点理论完备能完全避免死锁动态避免不需要一次性分配所有资源提高利用率提前预防在死锁发生前就拒绝危险请求缺点需要预知最大需求进程开始前必须声明 Max不现实进程数量固定不能动态创建/销毁进程资源数量固定不能动态增减资源开销大每次请求都要 O(n²×m) 的安全性检查可能导致饥饿大需求的进程可能长期等待