C++ 详解多线程中的线程同步与互斥量

Hazel
Hazel
发布时间:2026-10-16 | 2人浏览过
原创

在多线程编程中,当多个线程同时访问共享资源时,极易发生竞争条件导致数据错误。本文将基于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)。

互斥量的核心规则如下:

  1. 任何时候,至多只有一个线程可以锁定该互斥量。
  2. 试图对已经锁定的某一互斥量再次加锁,将可能导致线程阻塞或报错失败。
  3. 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。

一般实践中,针对每一类共享资源使用不同的互斥量,访问协议遵循以下步骤:

  • 针对共享资源锁定互斥量;
  • 进入临界区,访问共享资源;
  • 对互斥量解锁。

以下是具体的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++并发编程与进程控制的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表