linux21 线程同步--互斥锁
学习互斥锁之前我们引入以下问题:
我们创建五个线程分别对val进行++打印1000次:
#include<stdio.h>#include<pthread.h>#include<unistd.h>#include<stdlib.h>#include<semaphore.h>int val=0;void* fun(void*arg){for(int i=0;i<10000;i++){val++;printf("val=%d\n",val);}}int main(){pthread_t id[5];int i=0;for(;i<5;i++){pthread_create(&id[i],NULL,fun,NULL);}for(i=0;i<5;i++){pthread_join(id[i],NULL);}exit(0);}运行结果:

我们可以通过观察发现次代码并没有符合我们的预期打印50000,这是因为多个线程同时对val++造成的,为了解决这种现象我们利用互斥锁来来进行控制。
13.3.1互斥锁的基本概念
互斥锁是最常用的线程同步机制,用于保证在任意时刻只有一个线程可以访问特定的资源
- 竞态条件:当多个线程同时读写共享资源,且最终结果依赖于线程执行的先后顺序时,会出现不可预测的错误(如两个线程同时给计数器加 1,结果可能少加一次)。
- 互斥锁:本质是一个 “锁”,通过独占访问机制,保证同一时间只有一个线程能进入 “临界区”(操作共享资源的代码段),其他线程需等待锁释放后才能进入。
核心特性:
- 原子性:锁定操作是原子的,不会被中断
- 唯一性:一个线程锁定后,其他线程无法锁定,直到解锁
- 非繁忙等待:等待锁的线程会被挂起,不消耗CPU资源
13.3.2互斥锁的工作原理
互斥锁的状态有两种:未锁定(解锁)和锁定(持有)。其工作流程如下:
- 线程进入临界区前,尝试获取锁( lock ):
- 若锁处于 “未锁定” 状态,线程获取锁并将其设为 “锁定” 状态,进入临界区执行操作。
- 若锁处于 “锁定” 状态,线程会阻塞等待(进入休眠或循环检测),直到锁被释放。
- 线程离开临界区时,必须释放锁( unlock ),将锁设为 “未锁定” 状态,唤醒其他等待该锁的线程。
13.3.3互斥锁相应函数
函数原型 | 功能说明 |
|---|---|
int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) | 初始化互斥锁( attr 为属性,通常设为 NULL 使用默认属性)。 |
int pthread_mutex_lock( pthread_mutex_t *mutex) | 获取锁:若锁未被持有,则锁定并返回;否则阻塞等待。 |
int pthread_mutex_trylo ck(pthread_mutex_t *mutex) | 尝试获取锁:若锁未被持有,则锁定并返回 0;否则立即返回非 0(不阻塞)。 |
int pthread_mutex_unloc k(pthread_mutex_t *mutex) | 释放锁:将锁设为未锁定状态,唤醒等待的线程。 |
int pthread_mutex_destr oy(pthread_mutex_t *mutex) | 销毁互斥锁,释放资源。 |
1.初始化函数
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;动态初始化:
pthread_mutex_t mutex;pthread_mutex_init(&mutex, NULL); // NULL表示使用默认属性2.加锁解锁函数:
pthread_mutex_t mutex;pthread_mutex_lock(&mutex);加锁pthread_mutex_unlock(&mutex);解锁13.3.4关键注意事项
- 锁的粒度:
- 临界区应尽可能小(只包含操作共享资源的代码),避免长时间持有锁导致其他线程阻塞,降低并发效率。
- 反例:在锁内包含睡眠( sleep )、IO 操作等耗时任务,会严重影响性能。
- 避免死锁:死锁是指两个或多个线程互相等待对方释放锁,导致永久阻塞。常见原因及避免方法:
- 循环等待:线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1。解决:所有线程按固定顺序获取锁(如先锁 1 后锁 2)。
- 忘记释放锁:线程获取锁后因异常退出(如未处理的信号),未释放锁。解决:用 “资源获取即初始化(RAII)” 模式(C++ 中常用),在对象析构时自动释放锁。
- 递归锁与非递归锁:
- 默认互斥锁是非递归锁:同一线程多次获取同一锁会导致死锁(自身阻塞)。
- 若需同一线程多次获取锁(如递归函数中操作共享资源),需使用递归锁(通过属性 PTHREAD_MUTEX_RECURSIVE 初始化)。
- 互斥锁与信号量的区别:
- 互斥锁强调 “独占”,通常用于保护临界区,所有权明确(谁获取谁释放)。
- 信号量强调 “计数”,可用于控制多个线程同时访问资源(如限制 5 个线程同时读写文件)。
13.3.5总结
我们对以上代码进行修改:
#include<stdio.h>#include<pthread.h>#include<unistd.h>#include<stdlib.h>#include<semaphore.h>int val=0;pthread_mutex_t mutex;void* fun(void*arg){for(int i=0;i<10000;i++){pthread_mutex_lock(&mutex);val++;pthread_mutex_unlock(&mutex);printf("val=%d\n",val);}}int main(){pthread_t id[5];int i=0;pthread_mutex_init(&mutex,NULL);for(;i<5;i++){pthread_create(&id[i],NULL,fun,NULL);}for(i=0;i<5;i++){pthread_join(id[i],NULL);}pthread_mutex_destroy(&mutex);exit(0);}运行结果:

可以观察到使用信号量可以达到预期值50000.
