多线程编程核心概念:线程同步、异步、死锁、线程池

一、什么是多线程

多线程是计算机科学中的一个核心概念,它允许一个进程内同时运行多个执行流,每个执行流称为一个线程。这些线程共享进程的资源(如内存、文件句柄),但各自拥有独立的栈和寄存器状态,能够并行(在多核CPU上)或并发(在单核CPU上通过时间片轮转)地执行任务。

线程(Thread)

线程是操作系统调度的最小执行单位。

  1. 通信简单(直接共享内存)。线程之间共享:全局变量、堆内存
  2. 一个进程有多个线程。
进程(你的程序)
  ├── 线程1(主线程)
  ├── 线程2(采集线程)
  ├── 线程3(处理线程)

进程(Process)

进程指一个正在运行的程序。每个进程有独立内存空间、独立资源(文件句柄、句柄表等),但互相不能直接访问。
进程通信(IPC),常见方式:

  • 管道(pipe)
  • 共享内存
  • socket
  • 消息队列

单核 CPU

同一时刻只能执行一个线程。采用的是时间片轮转(操作系统调度)

时间线:

线程A → 线程B → 线程C → 线程A → ...

多核 CPU

每个核心可以执行一个线程。采用的是并行执行。

CPU 4核:

核心1 → 线程A
核心2 → 线程B
核心3 → 线程C
核心4 → 线程D

多线程可能带来的问题

一个进程里线程间共享:内存(全局变量、堆)

这就带来两个核心问题:

  1. 并发访问冲突(数据竞争)
  2. 执行顺序不可控

为什么多线程不一定更快?

核心原因1:线程切换有开销(上下文切换)
CPU在多个线程之间切换时,需要:保存当前线程状态(寄存器、栈)、恢复下一个线程状态。如果线程很多,大量时间花在“切换线程”,而不是“执行任务”

核心原因2:锁竞争(最常见性能杀手)

std::mutex mtx;

void func() {
    std::lock_guard<std::mutex> lock(mtx);
    // 操作共享数据
}

多个线程同时执行:

线程A:拿锁 ✔
线程B:等待 ❌
线程C:等待 ❌

线程数越多 → 抢锁越激烈 → 性能越差

本质原因:多线程变成,排队执行(串行化)

核心原因3:CPU核心数限制
如果线程数 > CPU核心数,例如:4核 CPU,开10个线程,会变成:一部分线程执行;一部分线程等待;不停切换

同步(Synchronous)

顺序执行,调用者必须等结果

异步(Asynchronous)

调用后立即返回,结果以后再拿


并发 vs 并行

  • 在单核CPU上,多线程通过操作系统快速切换时间片实现并发,微观上依次执行,宏观上看起来“同时”运行。
  • 在多核CPU上,多个线程可以真正同时运行,实现并行,提升计算效率。

线程调度

操作系统内核负责线程的调度,决定哪个线程获得CPU时间。调度策略包括抢占式(时间片用完强制切换)和协作式(线程主动让出CPU)。

三、线程同步

多线程最核心问题:多个线程同时操作同一数据


为什么需要锁?

例子:

int count = 0;

void add() {
    count++; // 不是原子操作
}

实际是三步,会出现数据竞争问题。多个线程同时读写共享数据时,可能产生不可预期的结果。两个线程同时给一个全局变量加1,最终结果可能比预期少:

1. 读
2. 加
3. 写

解决方案:使用互斥锁(mutex)、原子操作、读写锁等同步机制。

互斥锁(Mutex)

std::mutex mtx;

void func() {
    mtx.lock();
    // 临界区
    mtx.unlock();
}

改进写法(推荐):

std::lock_guard<std::mutex> lock(mtx);

原子操作(Atomic)

不用锁的轻量方案

std::atomic<int> count(0);
count++;

优点:

  • 无锁(快)
  • 线程安全

缺点:

  • 只能做简单操作

条件变量(Condition Variable)

用于线程等待/通知

std::condition_variable cv;
std::mutex mtx;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });
}

用途:

  • 生产者-消费者模型
  • 阻塞等待事件

四、死锁

什么是死锁

线程互相等待对方持有的资源,导致所有相关线程永远阻塞。

线程A:锁A → 等锁B  
线程B:锁B → 等锁A

预防:按固定顺序加锁,使用RAII管理锁,避免嵌套锁等。

死锁的四个条件

  1. 互斥
  2. 占有并等待
  3. 不可剥夺
  4. 循环等待

如何避免死锁

方法1:固定加锁顺序

// 所有地方都先锁 A 再锁 B

方法2:使用 std::lock

std::lock(mtx1, mtx2);

方法3:减少锁粒度

👉 锁越多越容易死锁


方法4:避免嵌套锁


五、线程池

为什么要线程池?

创建线程很贵:

std::thread t(...); // 系统调用 + 栈分配

频繁创建/销毁 → 性能差


线程池原理

核心结构:

任务队列 + 工作线程

流程:

  1. 提交任务
  2. 放入队列
  3. 空闲线程取任务执行

简化模型

        任务队列
           ↓
线程1 线程2 线程3

线程池优点

  • 复用线程(高性能)
  • 控制并发数量
  • 防止线程爆炸

常见实现

C++标准库

auto f = std::async(std::launch::async, func);

工业级(推荐)

  • Qt:QThreadPool

  • C++:

    • 自己实现
    • 或用第三方(boost/asio)

六、常见多线程模型

生产者-消费者模型

采集线程 → 队列 → 处理线程

主线程 + 工作线程

UI线程(Qt)
    ↓
后台线程处理任务

避免UI卡死

posted @ 2026-03-25 15:29  twjy  阅读(39)  评论(0)    收藏  举报