互斥
互斥量 (Mutex) 用于保证同一时刻只有一个线程进入临界区。它是最常见、最直接的同步原语,适合保护共享对象的一致性。
下文示例基于 <threads.h>;若目标工具链未提供该头文件,请把同一思路迁移到平台线程接口。
1. 生命周期
互斥量的典型流程是:mtx_init 初始化、mtx_lock 加锁、mtx_unlock 解锁、mtx_destroy 销毁。任何一条执行路径只要拿到锁,就必须保证最终释放。
c
#include <stdio.h>
#include <threads.h>
static mtx_t lock;
static int counter = 0;
int worker(void *arg) {
(void)arg;
for (int i = 0; i < 100000; ++i) {
mtx_lock(&lock);
counter++;
mtx_unlock(&lock);
}
return 0;
}
int main(void) {
thrd_t t1, t2;
if (mtx_init(&lock, mtx_plain) != thrd_success) {
return 1;
}
thrd_create(&t1, worker, NULL);
thrd_create(&t2, worker, NULL);
thrd_join(t1, NULL);
thrd_join(t2, NULL);
printf("counter = %d\n", counter);
mtx_destroy(&lock);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
可能的输出(示例):
bash
<输出与输入或平台相关,请以实际运行为准>
2. 易错点
忘记解锁会导致死锁;不同路径上以不一致顺序获取多个锁,会导致循环等待。并发代码出现“偶发卡死”时,首要检查点通常就是锁顺序与异常路径释放策略。
3. 实践建议
临界区应尽量短,只放必须受保护的读写;耗时操作放在锁外。这样既减少锁竞争,也降低把系统拖入阻塞链的概率。
4. 锁类型选择
mtx_init 的类型参数决定互斥量行为,例如普通锁、定时锁或递归锁。只有在确有语义需求时才使用更复杂类型;默认优先普通锁,通常更容易验证控制流和锁层级关系。
5. 锁顺序约定
当模块内存在多把锁时,建议固定全局获取顺序并写入文档。只要所有路径都遵守同一顺序,就能明显降低循环等待导致的死锁风险。
6. 失败路径上的解锁一致性
并发代码最容易出错的地方,不是“主路径忘了加锁”,而是失败路径忘了释放。若临界区中存在多个可能提前返回的分支,建议统一跳转到单一收尾出口,让加锁与解锁形成一一对应关系。
c
int update_shared(void) {
int rc = -1;
if (mtx_lock(&lock) != thrd_success) {
return -1;
}
if (!ready()) {
goto out_unlock;
}
rc = apply_change();
out_unlock:
mtx_unlock(&lock);
return rc;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
7. 互斥与原子的职责分层
互斥适合保护“需要整体一致”的复合状态;原子更适合单个对象的并发读写与轻量同步。若一个状态包含多个字段且需要保持跨字段关系,优先使用互斥通常更容易证明正确性。只有当状态边界足够简单且性能收益明确时,再考虑把局部路径替换为原子方案。