一、多线程对共享变量的非互斥访问
我们将要做的:构造多线程共享变量竞争的案例,并分析现象发生的原因,进而思考解决方式。
案例源代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
int num=30,count=10;
void *sub1(void *arg) {
int i = 0,tmp;
for (; i <count; i++){
tmp=num-1;
usleep(13);
num=tmp;
printf("线程1 num减1后值为: %d\n",num);
}
return ((void *)0);
}
void *sub2(void *arg){
int i=0,tmp;
for(;i<count;i++){
tmp=num-1;
usleep(31);
num=tmp;
printf("线程2 num减1后值为: %d\n",num);
}
return ((void *)0);
}
int main(int argc, char** argv) {
pthread_t tid1,tid2; // 两个子线程的id
int err,i=0,tmp;
void *tret; // 线程返回值
err=pthread_create(&tid1,NULL,sub1,NULL);
if(err!=0){
printf("pthread_create error:%s\n",strerror(err));
exit(-1);
}
err=pthread_create(&tid2,NULL,sub2,NULL);
if(err!=0){
printf("pthread_create error:%s\n",strerror(err));
exit(-1);
}
for(;i<count;i++){
tmp=num-1;
usleep(5);
num=tmp;
printf("main num减1后值为: %d\n",num);
}
printf("两个线程运行结束\n");
err=pthread_join(tid1,&tret);
if(err!=0){
printf("can not join with thread1:%s\n",strerror(err));
exit(-1);
}
printf("thread 1 exit code %d\n",(int)tret);
err=pthread_join(tid2,&tret);
if(err!=0){
printf("can not join with thread1:%s\n",strerror(err));
exit(-1);
}
printf("thread 2 exit code %d\n",(int)tret);
return 0;
}
🧠 1. 程序功能概述
创建了两个线程 sub1
和 sub2
,以及主线程三者共同对一个全局变量 num
执行减 1 操作,共减去 count * 3 = 30
次。
初始值:
int num = 30, count = 10;
所以理论上最终 num == 0
,但实际上并不一定!
⚠️ 2. 存在的核心问题:数据竞争(Race Condition)
❗ 对 num--
是分三步执行的:
tmp = num - 1;
usleep(x);
num = tmp;
这个过程不是原子操作,多个线程可能“交叉”访问这个变量,造成竞态条件(Race Condition)。
中间插入
usleep()
只是为了放大并发写入带来的冲突概率,模拟真实环境下的并发问题。
举例说明:
假设此时 num = 10
,两个线程同时读到:
线程1:tmp1 = 10 - 1 = 9,睡眠
线程2:tmp2 = 10 - 1 = 9,睡眠
然后:
线程1醒来执行 num = 9
线程2醒来执行 num = 9 (覆盖了线程1的操作)
🔴 这样 num 实际只减少了一次,而我们期望它减少两次(一个线程分别减少一次)!
🔍 3. 运行效果举例(输出可能类似):
线程1 num减1后值为: 29
线程2 num减1后值为: 28
main num减1后值为: 27
线程1 num减1后值为: 27 ←❗ 重复了
main num减1后值为: 26
线程2 num减1后值为: 26 ←❗ 再次重复
最终 num
的值可能 不是 0,甚至是更高。原因就是上面说的:很多次减法操作失效了。
✅4. 如何解决?使用线程同步机制:互斥锁 pthread_mutex_t
例如,添加全局互斥锁:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
将每个对 num
的访问部分用锁保护:
pthread_mutex_lock(&lock);
tmp = num - 1;
usleep(13); // 保留你原来的模拟处理
num = tmp;
pthread_mutex_unlock(&lock);
🔒 这样确保每次只有一个线程在访问和修改 num
。
🛠️ 5. 修改后关键片段示例(以 sub1
为例)
void *sub1(void *arg) {
int i = 0, tmp;
for (; i < count; i++) {
pthread_mutex_lock(&lock);
tmp = num - 1;
usleep(13);
num = tmp;
printf("线程1 num减1后值为: %d\n", num);
pthread_mutex_unlock(&lock);
}
return ((void *)0);
}
主线程、sub2
中也要加锁。
🔚 6. 总结
问题 | 说明 |
---|---|
数据竞争 | 多线程访问全局变量未加锁 |
后果 | num 最终值不确定,减法丢失 |
解决 | 使用 pthread_mutex 互斥锁 |
调试 | 建议加 -fsanitize=thread 或使用 valgrind --tool=helgrind 检查 |
二、并发线程同步与互斥
本次案例是在前一个案例「多线程对共享变量的非互斥访问」的基础上进行了修复和优化,引入了 pthread_mutex
互斥锁机制,实现了 多线程对共享变量的互斥访问控制,有效解决了数据竞争问题。
案例源代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
int num=30, count=10;
pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER;
void *sub1(void *arg) {
int i = 0,tmp;
for (; i<count; i++){
pthread_mutex_lock(&mylock); // 上锁
tmp=num-1;
usleep(13);
num=tmp;
pthread_mutex_unlock(&mylock); // 解锁
printf("线程1 第「%d」次num减1后值为: %d\n",i+1,num);
}
return ((void *)0);
}
void *sub2(void *arg){
int i=0,tmp;
for(;i<count;i++){
pthread_mutex_lock(&mylock); // 上锁
tmp=num-1;
usleep(31);
num=tmp;
pthread_mutex_unlock(&mylock); // 解锁
printf("线程2 第「%d」次num减1后值为: %d\n",i+1,num);
}
return ((void *)0);
}
int main(int argc, char** argv) {
pthread_t tid1,tid2;
int err,i=0,tmp;
void *tret; // 线程返回值
err=pthread_create(&tid1,NULL,sub1,NULL);
if(err!=0){
printf("pthread_create error:%s\n",strerror(err));
exit(-1);
}
err=pthread_create(&tid2,NULL,sub2,NULL);
if(err!=0){
printf("pthread_create error:%s\n",strerror(err));
exit(-1);
}
for(;i<count;i++){
pthread_mutex_lock(&mylock); // 主线程上锁
tmp=num-1;
usleep(5); //
num=tmp;
pthread_mutex_unlock(&mylock); // 主线程释放锁
printf("主线程 第「%d」次num减1后值为: %d\n",i+1,num);
}
printf("两个线程运行结束\n");
err=pthread_join(tid1,&tret);
if(err!=0){
printf("can not join with thread1:%s\n",strerror(err));
exit(-1);
}
printf("thread 1 exit code %d\n",(int)tret);
err=pthread_join(tid2,&tret);
if(err!=0){
printf("can not join with thread1:%s\n",strerror(err));
exit(-1);
}
printf("thread 2 exit code %d\n",(int)tret);
return 0;
}
下面从结构和功能两个维度逐段讲解这段代码。
✅ 1. 程序功能概述
多线程同时对一个共享变量
num
进行减 1 操作,为避免多个线程同时访问导致的数据错误,使用 互斥锁pthread_mutex_t
实现同步,确保线程间的安全访问。
🔧 2. 头文件说明
#include <stdio.h> // printf()
#include <stdlib.h> // exit()
#include <pthread.h> // pthread_create, pthread_join, pthread_mutex_*
#include <unistd.h> // usleep()
#include <string.h> // strerror()
✅ 上述头文件提供了线程创建、同步、休眠和错误信息处理等功能。
🔐 3. 全局变量及互斥锁
int num=30, count=10;
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
num
: 被所有线程共享的变量,初始为 30。count
: 每个线程循环次数。mylock
: 全局互斥锁,用于防止多个线程同时修改num
。
🧵 4. 线程函数 sub1 和 sub2
🔹 sub1
函数
void *sub1(void *arg) {
int i = 0, tmp;
for (; i < count; i++) {
pthread_mutex_lock(&mylock); // 上锁
tmp = num - 1;
usleep(13); // 模拟计算耗时
num = tmp;
pthread_mutex_unlock(&mylock); // 解锁
printf("线程1 第【%d】num减1后值为: %d\n", i, num);
}
return ((void *)0);
}
🔹 sub2
函数
void *sub2(void *arg) {
int i = 0, tmp;
for (; i < count; i++) {
pthread_mutex_lock(&mylock);
tmp = num - 1;
usleep(31);
num = tmp;
pthread_mutex_unlock(&mylock);
printf("线程2 第【%d】num减1后值为: %d\n", i, num);
}
return ((void *)0);
}
🟡 共同点:
- 都对共享变量
num
执行减 1 操作。 - 每次操作前后都加了互斥锁,保证了对
num
的访问是互斥的。
🟡 不同点:
- 睡眠时间不同,用于模拟线程调度差异,放大并发现象。
👑 5. 主线程 main
函数
1️⃣ 创建线程
pthread_create(&tid1, NULL, sub1, NULL);
pthread_create(&tid2, NULL, sub2, NULL);
- 启动两个子线程
sub1
和sub2
,它们会与主线程并发执行。
2️⃣ 主线程也参与对 num
的操作
for (; i < count; i++) {
pthread_mutex_lock(&mylock);
tmp = num - 1;
usleep(5);
num = tmp;
pthread_mutex_unlock(&mylock);
printf("main 第【%d】num减1后值为: %d\n", i, num);
}
💡 主线程和子线程一样,每次操作都加锁保护,保证不会同时操作 num
,从而避免数据冲突。
3️⃣ 等待线程结束并获取退出状态
pthread_join(tid1, &tret);
pthread_join(tid2, &tret);
- 使用
pthread_join()
等待线程结束。 - 线程的返回值是
NULL
(即return ((void*)0)
),打印时结果是0
。
🧪 6. 运行效果示例(输出节选)
主线程 第「1」次num减1后值为: 29
主线程 第「2」次num减1后值为: 28
主线程 第「3」次num减1后值为: 27
线程1 第「1」次num减1后值为: 26
线程1 第「2」次num减1后值为: 25
...
最终:
两个线程运行结束
thread 1 exit code 0
thread 2 exit code 0
🔒 7. 为什么这次没有竞态条件?
因为你加入了 pthread_mutex_lock()
和 pthread_mutex_unlock()
对每一次对 num
的读-改-写操作进行了加锁保护,确保每次修改都是互斥的、完整的。
📌 八、总结
项目 | 说明 |
---|---|
多线程创建 | 使用 pthread_create 创建两个线程 |
共享资源 | 所有线程和主线程共享变量 num |
数据保护 | 使用 pthread_mutex 实现访问同步,防止竞态 |
同步结束 | 使用 pthread_join 等待子线程结束 |
延时模拟 | 使用 usleep() 模拟并发中的调度差异 |