当前位置: 首页 > news >正文

线程控制:互斥与同步

在多线程编程中,多个线程共享进程资源是提高效率的关键,但同时也带来了临界资源访问冲突的问题。比如多个线程同时操作一个变量、一块内存或一个文件时,可能会导致数据错乱、结果不可预期。解决这类问题的核心就是互斥同步机制 —— 前者保证资源的排他性访问,后者保证线程间操作的有序性。

一、互斥:临界资源的 "排他通行证"

1. 什么是互斥?

互斥指的是在多线程环境中,对临界资源(如共享变量、文件、内存块等)的访问必须是排他性的:同一时刻只能有一个线程操作该资源,其他线程需等待当前线程释放资源后才能访问。

临界资源的典型特征是 "多线程共享且修改",例如多个线程同时对一个计数器进行自增操作,若不控制互斥,最终结果可能远小于预期值(因指令交错执行)。

2. 互斥的实现:互斥锁(Mutex)

互斥锁是实现互斥的核心机制,它像一把 "锁",线程访问临界资源前需 "上锁",访问完毕后 "解锁"。未获得锁的线程会阻塞等待,直到锁被释放。

(1)互斥锁的核心要素
  • 类型pthread_mutex_t(POSIX 标准定义的互斥锁类型)
  • 本质:内核对象,用于标记资源的占用状态
  • 操作原则:加锁(lock)和解锁(unlock)之间的代码为原子操作(不可分割,要么全执行,要么全不执行)
(2)互斥锁的使用框架

互斥锁的使用需遵循固定流程:定义→初始化→加锁→解锁→销毁,每个步骤对应特定函数:

步骤函数功能描述
定义锁pthread_mutex_t mutex;声明一个互斥锁变量(内核对象)
初始化锁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_unlock(pthread_mutex_t *mutex);释放锁,唤醒等待该锁的线程
销毁锁int pthread_mutex_destroy(pthread_mutex_t *mutex);释放锁占用的资源(必须在所有线程解锁后执行)
(3)关键函数详解
  • 初始化锁(pthread_mutex_init

    • 参数mutex:指向要初始化的锁变量的指针;
    • 参数attr:锁的属性(如共享范围、类型等),NULL表示默认锁(普通互斥锁);
    • 返回值:成功返回 0,失败返回非 0 错误码。
  • 加锁(pthread_mutex_lock

    • 若锁未被占用,当前线程获取锁,继续执行;
    • 若锁已被占用,当前线程进入阻塞状态(放弃 CPU,等待锁释放);
    • 加锁后到解锁前的代码为 "临界区",确保唯一线程访问。
  • 非阻塞加锁(pthread_mutex_trylock

    • 功能与pthread_mutex_lock类似,但不阻塞:若锁已被占用,直接返回非 0 错误码(如EBUSY),线程可执行其他逻辑。
(4)示例:用互斥锁解决共享变量冲突

下面的代码模拟 10 个线程争夺 3 个资源的场景,通过互斥锁保证资源计数的正确性:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>int count = 3; // 共享资源:3个可用资源
pthread_mutex_t mutex; // 定义互斥锁void *thread_func(void *arg) {while (1) {// 加锁:进入临界区pthread_mutex_lock(&mutex);if (count > 0) {printf("线程%d获取资源,剩余%d个\n", *((int*)arg), --count);pthread_mutex_unlock(&mutex); // 解锁:退出临界区break; // 获取资源后退出循环} else {printf("线程%d等待资源...\n", *((int*)arg));pthread_mutex_unlock(&mutex); // 解锁:避免死锁// 短暂休眠,让其他线程有机会获取锁usleep(100000);}}return NULL;
}int main() {pthread_t tid[10];int ids[10];// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建10个线程for (int i = 0; i < 10; i++) {ids[i] = i;pthread_create(&tid[i], NULL, thread_func, &ids[i]);}// 等待所有线程结束for (int i = 0; i < 10; i++) {pthread_join(tid[i], NULL);}// 销毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}

3. 互斥锁练习

练习 1:多线程安全写入字符数组

设计多线程程序,让 5 个线程向同一块字符数组写入不同字符串,要求同一时刻只有一个线程操作数组(用堆区互斥锁,即动态分配的锁)。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>char shared_buf[1024] = {0}; // 共享字符数组
pthread_mutex_t *mutex; // 堆区互斥锁void *write_func(void *arg) {char *str = (char*)arg;// 加锁:独占访问数组pthread_mutex_lock(mutex);strcat(shared_buf, str);printf("写入后数组内容:%s\n", shared_buf);// 解锁:释放访问权pthread_mutex_unlock(mutex);return NULL;
}int main() {pthread_t tid[5];char *strs[5] = {"Hello ", "World ", "from ", "thread ", "!"};// 堆区分配并初始化互斥锁mutex = malloc(sizeof(pthread_mutex_t));pthread_mutex_init(mutex, NULL);// 创建线程写入数据for (int i = 0; i < 5; i++) {pthread_create(&tid[i], NULL, write_func, strs[i]);}// 等待所有线程结束for (int i = 0; i < 5; i++) {pthread_join(tid[i], NULL);}// 销毁锁并释放堆内存pthread_mutex_destroy(mutex);free(mutex);return 0;
}

二、同步:线程间的 "有序协作"

1. 什么是同步?

同步指的是多线程在访问资源时,需按照预定的先后顺序执行(如 "先生产后消费")。互斥锁仅保证排他性,无法控制执行顺序,而同步机制则解决这一问题。

例如:线程 A 负责向缓冲区写数据,线程 B 负责读数据,必须保证 "线程 A 写完后,线程 B 才能读",这就是同步的典型场景。

2. 同步的实现:信号量(Semaphore)

信号量是实现同步的核心机制,它通过一个计数器控制线程的执行顺序:线程需先 "申请" 信号量(计数器减 1),若计数器为 0 则阻塞;执行完毕后 "释放" 信号量(计数器加 1),唤醒阻塞的线程。

(1)信号量的分类
  • 无名信号量:用于线程间同步(共享于同一进程的线程),需初始化在共享内存(如堆区);
  • 有名信号量:用于进程间同步(通过文件系统路径标识),本文重点讲解无名信号量。
(2)信号量的使用框架

与互斥锁类似,信号量的使用流程为:定义→初始化→P 操作→V 操作→销毁

步骤函数功能描述
定义信号量sem_t sem;声明信号量变量
初始化int sem_init(sem_t *sem, int pshared, unsigned int value);初始化信号量,pshared=0表示线程间使用,value为初始计数器值
P 操作int sem_wait(sem_t *sem);申请资源:计数器减 1,若结果 < 0 则阻塞等待
V 操作int sem_post(sem_t *sem);释放资源:计数器加 1,唤醒阻塞的线程
销毁int sem_destroy(sem_t *sem);释放信号量占用的资源
(3)关键函数详解
  • 初始化(sem_init

    • 参数sem:指向信号量变量的指针;
    • 参数pshared:0 表示线程间使用,非 0 表示进程间使用;
    • 参数value:初始计数器值(例如:value=1表示 "互斥信号量",value=0表示 "同步信号量");
    • 返回值:成功返回 0,失败返回 - 1。
  • P 操作(sem_wait

    • 功能:申请资源。计数器先减 1,若结果≥0,线程继续执行;若结果 < 0,线程阻塞。
    • 例如:初始value=0时,第一个执行sem_wait的线程会阻塞,直到其他线程执行sem_post(计数器变为 1)。
  • V 操作(sem_post

    • 功能:释放资源。计数器加 1,若有线程因该信号量阻塞,则唤醒其中一个。
(4)示例:用信号量实现 "输入 - 统计" 同步

需求:线程 A 获取用户输入,线程 B 统计输入长度,要求 B 必须在 A 输入后执行,输入 "quit" 时程序结束。

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <string.h>char input_buf[1024] = {0};
sem_t sem; // 同步信号量:控制B在A之后执行// 线程A:获取用户输入
void *input_thread(void *arg) {while (1) {printf("请输入内容(输入quit退出):");fgets(input_buf, sizeof(input_buf), stdin);// 移除换行符input_buf[strcspn(input_buf, "\n")] = '\0';// V操作:通知B可以统计(计数器+1)sem_post(&sem);// 若输入quit,退出循环if (strcmp(input_buf, "quit") == 0) break;}return NULL;
}// 线程B:统计输入长度
void *count_thread(void *arg) {while (1) {// P操作:等待A输入(计数器-1,若为0则阻塞)sem_wait(&sem);if (strcmp(input_buf, "quit") == 0) break;printf("输入长度:%ld\n", strlen(input_buf));}return NULL;
}int main() {pthread_t tidA, tidB;// 初始化信号量:线程间使用,初始值0(B先阻塞,等待A的V操作)sem_init(&sem, 0, 0);// 创建线程pthread_create(&tidA, NULL, input_thread, NULL);pthread_create(&tidB, NULL, count_thread, NULL);// 等待线程结束pthread_join(tidA, NULL);pthread_join(tidB, NULL);// 销毁信号量sem_destroy(&sem);return 0;
}

3. 同步练习:火车票售票系统

需求:2 个售票窗口(线程)共卖 100 张票,要求不重复售票,输出格式如 "窗口 1 卖出车票 1"。

思路:用互斥锁保证票数的原子操作,用信号量控制初始可售票数(value=100)。

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>int ticket_count = 100; // 总票数
pthread_mutex_t mutex; // 互斥锁:保护ticket_count
sem_t sem; // 信号量:控制可售票数void *sell_ticket(void *arg) {int window = *((int*)arg);while (1) {// P操作:申请售票资格(若票已售完则阻塞)sem_wait(&sem);// 加锁:修改票数pthread_mutex_lock(&mutex);if (ticket_count <= 0) {pthread_mutex_unlock(&mutex);break;}printf("窗口%d 卖出车票%d\n", window, ticket_count--);pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t tid1, tid2;int win1 = 1, win2 = 2;// 初始化互斥锁和信号量pthread_mutex_init(&mutex, NULL);sem_init(&sem, 0, 100); // 初始100张票// 创建售票窗口线程pthread_create(&tid1, NULL, sell_ticket, &win1);pthread_create(&tid2, NULL, sell_ticket, &win2);// 等待线程结束pthread_join(tid1, NULL);pthread_join(tid2, NULL);// 清理资源pthread_mutex_destroy(&mutex);sem_destroy(&sem);return 0;
}

三、死锁:多线程编程的 "陷阱"

1. 什么是死锁?

死锁是指多个线程因争夺资源而陷入无限等待的状态。例如:线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1,两者永远无法继续执行。

2. 死锁的四个必要条件

死锁的产生必须同时满足以下四个条件,破坏任意一个即可避免死锁:

  1. 互斥条件:资源只能被一个线程占用;
  2. 请求与保持条件:线程持有部分资源,同时请求其他资源;
  3. 不剥夺条件:线程已获得的资源不能被强行剥夺;
  4. 循环等待条件:线程间形成资源请求的循环链(如 A 等 B,B 等 A)。

3. 避免死锁的建议

  • 固定顺序申请资源(如所有线程先申请锁 1,再申请锁 2);
  • 限时申请资源(用pthread_mutex_trylock,超时则释放已占资源);
  • 减少资源持有时间(临界区代码尽量简短)。

总结

  • 互斥通过互斥锁实现临界资源的排他访问,核心是pthread_mutex_lock/unlock
  • 同步通过信号量控制线程执行顺序,核心是sem_wait(P 操作)和sem_post(V 操作);
  • 死锁是多线程编程的常见问题,需通过合理设计避免四个必要条件的同时满足。

掌握互斥与同步机制,是编写安全、高效多线程程序的基础。实际开发中需结合具体场景选择合适的工具,平衡性能与安全性。

http://www.lryc.cn/news/592787.html

相关文章:

  • math.h函数
  • 深度学习零基础入门(3)-图像与神经网络
  • 需求变更频繁?构建动态估算机制四大要点
  • 短视频矩阵系统:选择与开发的全面指南
  • nastools继任者?极空间部署影视自动化订阅系统『MediaMaster』
  • 代理模式及优化
  • 解锁时序数据库选型密码,为何国产开源时序数据库IoTDB脱颖而出?
  • 脉冲神经网络(Spiking Neural Network, SNN)与知识蒸馏(Knowledge Distillation, KD)
  • Vue3 Anime.js超级炫酷的网页动画库详解
  • Kubernetes (k8s)、Rancher 和 Podman 的异同点分析
  • Jmeter系列(6)-测试计划
  • 网关-微服务网关实现
  • Postman/Apipost中使用Post URL编码发送含换行符参数的问题分析
  • vue2 面试题及详细答案150道(101 - 120)
  • 智慧后厨检测算法构建智能厨房防护网
  • Redis学习其三(订阅发布,主从复制,哨兵模式)
  • 【大模型:知识图谱】--6.Neo4j DeskTop安装+使用
  • RS485转PROFIBUS DP网关写入命令让JRT激光测距传感器开启慢速模式连续测量
  • CCF编程能力等级认证GESP—C++1级—20250628
  • FLTK UI窗口关闭时延时卡顿问题全流程分析与优化实战
  • C++算法竞赛篇:DevC++ 如何进行debug调试
  • 记录DataGrip 2025.1.3破解失败后,无法重启问题修复
  • 第16章 基于AB实验的增长实践——验证想法:AB实验实践
  • pom.xml文件中的${}变量从哪里传值
  • UniApp TabBar 用户头像方案:绕过原生限制的实践
  • React + Mermaid 图表渲染消失问题剖析及 4 种代码级修复方案
  • Java异步日志系统性能优化实践指南:基于Log4j2异步Appender与Disruptor
  • Camera相机人脸识别系列专题分析之十七:人脸特征检测FFD算法之libhci_face_camera_api.so 296点位人脸识别检测流程详解
  • CentOS 7 配置环境变量常见的4种方式
  • 虚拟机centos服务器安装