C++ 详解多线程中的线程同步与互斥量
在多线程编程中,当多个线程同时访问共享资源时,极易发生竞争条件导致数据错误。本文将基于POSIX线程(pthread)标准,剖析C/C++环境下的线程同步机制,重点讲解如何利用互斥量(Mutex)保护临界区,确保数据一致性与执行安全。
线程同步问题分析
线程的主要优势在于可以通过全局变量高效地共享信息。然而,这种便捷性伴随着风险:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在被其他线程修改的变量。当这种现象发生时,便需要引入“线程同步”。
线程同步定义:当一个线程对内存进行操作时,其他线程不能对该地址进行操作,直到当前线程完成操作。在此过程中,其他线程处于等待状态。
临界区(Critical Section):指访问某一共享资源的代码片段。这段代码的执行应为原子操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。
需要注意的是,线程同步会降低并发效率,但在保证数据正确性的前提下是必须的。
以下代码展示了未使用同步机制时的典型票务处理案例:创建3个子线程并发销售100张门票。
/*
使用多线程实现买票的案例。
有3个窗口,一共是100张票。
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;
void * sellticket(void * arg) {
// 卖票
while(tickets > 0) {
usleep(6000); //模拟网络延迟或处理时间
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
}
return NULL;
}
int main() {
// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);
// 回收子线程的资源,默认阻塞状态
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
// 设置线程分离。
// pthread_detach(tid1);
// pthread_detach(tid2);
// pthread_detach(tid3);
pthread_exit(NULL); // 退出主线程
return 0;
}上述程序运行后通常会产生两类严重错误:
- 重复售卖:同一张票被打印出多次出售记录;
- 非法计数:出现售票数为
0或-1的情况,违背业务逻辑。
原因分析:由于加入了 usleep 延迟,三个线程在获取CPU时间片后进入执行流。在读取票数并执行减一操作之间,时间片可能发生切换,导致多个线程读取到相同的票数并在后续递减中产生冲突。这是典型的“检查-然后-执行”(Check-then-Act)竞争场景。
互斥量解决方案
为避免线程更新共享变量时出现问题,可以使用互斥量(Mutex)来确保同时仅有一个线程可以访问某项共享资源。互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。
互斥量的核心规则如下:
- 任何时候,至多只有一个线程可以锁定该互斥量。
- 试图对已经锁定的某一互斥量再次加锁,将可能导致线程阻塞或报错失败。
- 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。
一般实践中,针对每一类共享资源使用不同的互斥量,访问协议遵循以下步骤:
- 针对共享资源锁定互斥量;
- 进入临界区,访问共享资源;
- 对互斥量解锁。
以下是具体的C/C++实现代码。在此示例中,我们将票数调整为1000张,并通过 pthread_mutex_t 类型及其相关函数实现安全控制。
第1步:声明与初始化互斥量
首先在全局作用域定义互斥量变量,并在主线程创建子线程前对其进行初始化。 restrict 关键字提示编译器该指针为唯一访问路径。
// 创建一个互斥量 -- 在全局区创建 pthread_mutex_t mutex; // 在主函数中初始化 pthread_mutex_init(&mutex, NULL); // attr参数设为NULL表示使用默认属性
第2步:编写加锁/解锁逻辑
在线程函数中,必须在访问共享变量 tickets 之前调用 pthread_mutex_lock 尝试获取锁。若锁已被占用,当前线程将阻塞等待。
void * sellticket(void * arg) {
// 卖票
while(1) {
// 1. 加锁:若有一个线程加锁了,那么其他的线程只能阻塞等待
pthread_mutex_lock(&mutex);
// 2. 访问临界区
if(tickets > 0) {
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
} else {
// 票数售罄,跳出循环
break;
}
// 3. 解锁:让出锁以便其他线程工作
pthread_mutex_unlock(&mutex);
}
return NULL;
}第3步:完整资源释放
所有线程结束并回收资源后,需销毁互斥量以释放系统资源。
int main() {
// ... 前置初始化代码 ...
// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);
// 回收子线程的资源,阻塞
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_exit(NULL); // 退出主线程
// 释放互斥量资源
pthread_mutex_destroy(&mutex);
return 0;
}通过上述封装,无论线程调度如何频繁切换,任何时刻只有一个线程能修改 tickets 值,从而彻底消除了数据竞争带来的负数或重复售出问题。
补充说明:非阻塞尝试加锁
除了标准的阻塞式 pthread_mutex_lock 外,POSIX线程库还提供了 pthread_mutex_trylock 函数。如果互斥量已经被其他线程锁定,trylock 不会使当前线程阻塞挂起,而是立即返回一个非零错误码(通常为 EBUSY)。开发者可根据业务场景选择是否继续重试或执行降级逻辑。
以上就是C++/C多线程中利用互斥量实现线程同步的详细内容,更多关于C++并发编程与进程控制的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













