多线程编程中常见的"偶现"问题源于多个线程同时操作共享资源,而线程同步的核心在于控制谁能访问资源以及何时访问。下面我们深入探讨各种同步机制的本质区别。


一、线程安全问题根源

如示例代码所示:

int count = 0;

void* task(void* arg)
{
    for(int i = 0; i < 100000; i++)
    {
        count++;  // 非原子操作
    }
    return nullptr;
}

count++实际包含三个步骤:

  1. 读取 count 值
  2. 执行 count + 1
  3. 写回新值

当多个线程同时执行时,可能出现:

线程A:读取 count=10
线程B:读取 count=10
线程A:写入 11
线程B:写入 11  // 实际应得12

结果常小于预期值 200000。


二、同步的本质

线程同步并非让线程同时运行,而是:让线程有序访问资源

例如:线程A先访问资源,线程B等待。或者线程A完成任务后,通知线程B继续执行


三、互斥锁:基础保护

核心原理:同一时刻仅一个线程访问资源

工作流程

graph LR
A[线程A加锁] --> B[访问资源]
B --> C[释放锁]
C --> D[线程B获得锁]

代码示例

#include <iostream>
#include <pthread.h>

using namespace std;

// 全局共享变量
// 两个线程都会对它进行修改
int count = 0;

// 定义互斥锁(mutex)
// 用来保护共享资源 count
pthread_mutex_t mutex;

// 线程执行函数
// pthread 创建线程时,线程入口函数必须是这种格式
void* task(void* arg)
{
    // 循环执行 10 万次
    for(int i = 0; i < 100000; i++)
    {
        // 加锁
        // 同一时刻只允许一个线程进入临界区
        pthread_mutex_lock(&mutex);

        // 临界区代码(共享资源操作)
        // 如果不加锁,多个线程同时执行 count++
        // 会导致数据错误
        count++;

        // 解锁
        // 释放资源访问权
        pthread_mutex_unlock(&mutex);
    }

    return nullptr;
}

int main()
{
    // 定义两个线程对象
    pthread_t t1, t2;

    // 初始化互斥锁
    // nullptr 表示使用默认属性
    pthread_mutex_init(&mutex, nullptr);

    // 创建线程 t1
    // 参数说明:
    // &t1      -> 线程对象
    // nullptr  -> 默认线程属性
    // task     -> 线程执行函数
    // nullptr  -> 传递给线程函数的参数
    pthread_create(&t1, nullptr, task, nullptr);

    // 创建线程 t2
    pthread_create(&t2, nullptr, task, nullptr);

    // 等待线程 t1 执行结束
    // 如果不等待,主线程可能提前退出
    pthread_join(t1, nullptr);

    // 等待线程 t2 执行结束
    pthread_join(t2, nullptr);

    // 输出最终结果
    // 正常情况下应该输出 200000
    cout << count << endl;

    // 销毁互斥锁
    // 释放系统资源
    pthread_mutex_destroy(&mutex);

    return 0;
}

缺点

读操作也需要互斥,降低并发效率


四、读写锁:读多写少优化

核心规则

允许多线程并发读
写操作需独占访问

读写锁流程

线程A读
线程B读
线程C读

可以同时进行

但线程D写,那么所有线程必须等待

为什么读写锁性能更高?

因为现实开发中:“读”远远多于“写”

例如:查询用户信息,读取缓存,查看配置。大部分时候只能读取数据,因此读写锁并发能力更强

读操作频率 >> 写操作频率

读写锁可提升 3-5 倍并发性能。


五、条件变量:线程协作

解决场景

生产者-消费者模型中:

// 低效方案
while(true) {
    if(数据就绪) 消费;  // 空转消耗CPU
}

工作流程

graph TB
A[消费者等待] --> B[生产者工作]
B --> C[发送通知]
C --> D[消费者唤醒]

本质

没有条件满足,线程就睡眠

条件满足后,再唤醒线程


六、信号量:资源配额管理

与互斥锁区别

机制 准入数量
互斥锁 1
信号量 N

典型应用

连接池管理:sem=5 表示允许5个并发连接

重要性

因为现实开发中:很多资源并不是只能一个线程访问。例如:数据库连接池,线程池,网络连接池都属于:有限资源共享。


七、同步机制对比

机制 核心目标 适用场景
互斥锁 资源独占访问 通用共享资源保护
读写锁 优化读并发 读多写少场景
条件变量 线程状态协作 生产者-消费者模型
信号量 资源配额控制 连接池/线程池管理

 

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐