引言
在上一篇文章中,我们学习了线程的创建、退出和等待机制。但我们留下了一个重要问题没有解决------线程同步。
当多个线程同时访问共享变量时,由于线程调度的不确定性,可能会出现意想不到的错误结果。今天,我们将深入探讨这个问题的根源,并学习如何使用互斥锁(Mutex) 来解决线程同步问题。
第一部分:多线程共享变量的竞态条件
一、问题演示:全局变量累加
先看一个简单的例子:创建5个线程,每个线程对同一个全局变量进行1000次自增操作。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUM 5
#define LOOP_COUNT 1000
int g_count = 1; // 全局共享变量
void* thread_func(void* arg) {
for (int i = 0; i < LOOP_COUNT; i++) {
g_count++;
// 每次自增后打印当前值
printf("%d\n", g_count);
}
return NULL;
}
int main() {
pthread_t tids[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&tids[i], NULL, thread_func, NULL);
}
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(tids[i], NULL);
}
printf("最终结果: %d\n", g_count);
return 0;
}
预期结果:
-
初始值:1
-
每个线程增加1000次,5个线程共增加5000次
-
最终值应为:1 + 5000 = 5001
实际运行结果:
-
单核处理器:总是输出5001
-
多核处理器:经常输出小于5001(如4999、4998等)
二、现象分析
运行结果观察:
第1次运行: 5001
第2次运行: 5001
第3次运行: 4999 ← 丢失了2次累加
第4次运行: 5001
第5次运行: 4998 ← 丢失了3次累加
关键发现:
-
单处理器环境下,问题不会出现
-
多处理器环境下,问题出现概率与处理器数量相关
-
处理器越多,出现问题的概率越高
第二部分:问题的根本原因
一、a++ 不是原子操作
在C语言中,a++ 看起来是一条语句,但在硬件层面,它被分解为多个步骤:
二、竞态条件的产生过程
当两个线程并行执行时,可能出现以下情况:

三、竞态条件的本质
| 条件 | 说明 |
|---|---|
| 共享资源 | 多个线程可以访问的变量或资源 |
| 非原子操作 | 操作可以被中断,不是一步完成的 |
| 并行执行 | 多处理器同时执行,导致操作交错 |
| 结果不可预测 | 最终结果取决于线程调度的具体顺序 |
第三部分:解决方案------互斥锁(Mutex)
一、互斥锁的概念
互斥锁(Mutex,Mutual Exclusion)是一种同步机制,确保同一时刻只有一个线程可以进入临界区。
二、互斥锁的核心操作
| 函数 | 作用 | 说明 |
|---|---|---|
pthread_mutex_init |
初始化互斥锁 | 使用前必须初始化 |
pthread_mutex_lock |
加锁 | 如果锁已被占用,阻塞等待 |
pthread_mutex_unlock |
解锁 | 释放锁,唤醒等待线程 |
pthread_mutex_destroy |
销毁互斥锁 | 使用完毕后销毁 |
三、使用互斥锁解决竞态条件
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUM 5
#define LOOP_COUNT 1000
int g_count = 1;
pthread_mutex_t mutex; // 定义互斥锁
void* thread_func(void* arg) {
for (int i = 0; i < LOOP_COUNT; i++) {
pthread_mutex_lock(&mutex); // 加锁
g_count++; // 临界区
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
int main() {
pthread_t tids[THREAD_NUM];
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&tids[i], NULL, thread_func, NULL);
}
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(tids[i], NULL);
}
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("最终结果: %d\n", g_count); // 总是输出 5001
return 0;
}
第四部分:互斥锁的更多细节
一、互斥锁与信号量的关系
| 特性 | 互斥锁 | 信号量(初值为1) |
|---|---|---|
| 本质 | 二进制锁 | 计数器 |
| 操作 | lock/unlock | P/V |
| 所有权 | 只有加锁的线程才能解锁 | 任何线程都可以执行V操作 |
| 适用场景 | 保护临界区 | 资源计数、同步 |
二、互斥锁的完整接口
cpp
#include <pthread.h>
// 静态初始化(适用于全局变量)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 动态初始化
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
// 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试加锁(非阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
三、互斥锁的使用规范
cpp
// 规范1:加锁后必须在所有退出路径上解锁
void good_function() {
pthread_mutex_lock(&mutex);
if (error_condition) {
pthread_mutex_unlock(&mutex);
return;
}
// 正常处理
pthread_mutex_unlock(&mutex);
}
// 规范2:临界区代码应尽量简短
void good_critical_section() {
pthread_mutex_lock(&mutex);
// 只保护必要的最小代码段
count++;
pthread_mutex_unlock(&mutex);
// 耗时操作放在临界区之外
do_expensive_work();
}
// 规范3:避免死锁------不要嵌套加锁
void avoid_deadlock() {
// 如果需要多个锁,确保固定顺序
pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_b);
// ...
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
}
第五部分:线程同步的应用场景
一、典型场景对比
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 卖票系统 | 100张票卖出102张 | 互斥锁保护票数变量 |
| 账户转账 | 并发取款导致余额错误 | 互斥锁保护余额操作 |
| 日志系统 | 多条日志交错输出 | 互斥锁保护写入操作 |
| 生产者-消费者 | 缓冲区数据不一致 | 条件变量 + 互斥锁 |
二、卖票系统示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int tickets = 100; // 总票数
pthread_mutex_t mutex;
void* sell_ticket(void* arg) {
int tid = *(int*)arg;
while (1) {
pthread_mutex_lock(&mutex);
if (tickets > 0) {
printf("线程%d售出第%d张票\n", tid, tickets);
tickets--;
pthread_mutex_unlock(&mutex);
usleep(100); // 模拟售票耗时
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return NULL;
}
int main() {
pthread_t tids[5];
int ids[5];
pthread_mutex_init(&mutex, NULL);
for (int i = 0; i < 5; i++) {
ids[i] = i + 1;
pthread_create(&tids[i], NULL, sell_ticket, &ids[i]);
}
for (int i = 0; i < 5; i++) {
pthread_join(tids[i], NULL);
}
pthread_mutex_destroy(&mutex);
printf("剩余票数:%d\n", tickets);
return 0;
}
第六部分:Ubuntu帮助手册安装
一、安装POSIX帮助手册
Ubuntu 20.04及之前版本需要手动安装线程相关帮助文档:
cpp
# 安装帮助手册
sudo apt update
sudo apt install manpages-posix-dev
# 验证安装
man pthread_mutex_init
man pthread_mutex_lock
man pthread_mutex_unlock
二、帮助手册的使用
cpp
# 查看函数原型和说明
man pthread_mutex_init
# 查看错误码
man pthread_mutex_lock
# 搜索相关函数
man -k pthread_mutex
第七部分:并发与并行的概念回顾
一、并发 vs 并行
| 概念 | 定义 | 硬件要求 | 执行特征 |
|---|---|---|---|
| 并发 | 多个任务交替执行 | 单处理器即可 | 某一时刻只有一个任务在执行 |
| 并行 | 多个任务同时执行 | 多处理器 | 某一时刻有多个任务在执行 |
二、对线程同步的影响

总结
一、核心概念对比
| 概念 | 说明 |
|---|---|
| 竞态条件 | 多线程同时访问共享资源导致的结果不确定性 |
| 原子操作 | 不可分割的操作(硬件层面) |
| 临界区 | 需要互斥保护的代码段 |
| 互斥锁 | 保证同一时刻只有一个线程进入临界区 |
二、互斥锁核心函数
| 函数 | 作用 | 调用时机 |
|---|---|---|
pthread_mutex_init |
初始化 | 创建线程之前 |
pthread_mutex_lock |
加锁 | 进入临界区之前 |
pthread_mutex_unlock |
解锁 | 退出临界区之后 |
pthread_mutex_destroy |
销毁 | 所有线程结束后 |
三、使用规范总结
| 规范 | 说明 |
|---|---|
| 临界区最小化 | 只保护必要的代码 |
| 解锁路径完整 | 所有退出路径都要解锁 |
| 避免嵌套锁 | 防止死锁 |
| 初始化与销毁 | 配对使用 |
本篇文章介绍了多线程编程中的核心问题------竞态条件,以及使用互斥锁解决问题的标准方法。
核心要点回顾:
-
a++不是原子操作,多线程并行执行时可能出错 -
互斥锁确保同一时刻只有一个线程进入临界区
-
加锁/解锁必须配对,避免死锁
-
临界区代码应尽量简短
学习建议:
-
编写多线程代码时,始终考虑共享资源的保护
-
使用互斥锁保护所有共享变量的访问
-
临界区越短越好
-
学会使用帮助手册查询函数接口