【操作系统(Linux)】——多线程对共享变量访问的同步与互斥

发布于:2025-04-10 ⋅ 阅读:(39) ⋅ 点赞:(0)

一、多线程对共享变量的非互斥访问

我们将要做的:构造多线程共享变量竞争的案例,并分析现象发生的原因,进而思考解决方式。

案例源代码:

#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. 程序功能概述

创建了两个线程 sub1sub2,以及主线程三者共同对一个全局变量 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);
  • 启动两个子线程 sub1sub2,它们会与主线程并发执行。

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() 模拟并发中的调度差异