Skip to content

C++ Multithread

线程

查询

  • 线程id
  • 硬件支持的线程数量
auto main() ->  int {   
    std::cout   <<  "Thread ID: "   << std::this_thread::get_id()   <<  '\n';   
    std::cout << std::thread::hardware_concurrency() << '\n';
}   

sleep

std::this_thread::sleep_for(std::chrono::seconds{1});   

thread

头文件 thread , worker_thread 是工作的函数,后面(1)是参数给那个函数的

std::thread worker(worker_thread, 1); 

等待工作完成

worker.join();

也可以 detach

t2.detach();  // 分离线程,不再管理

GDB

info thread

数据竞争解决方案

如果数据不会改变,就没有竞争问题

Atomic operation

std::atomic<int> counter{0};
void safe_increment() {
    counter++;  // 这是原子的,安全!
}
std::println("counter: {}", counter.load());

底层实现是硬件指令 Compare-and-Swap (CAS)

Mutex

先定义 std::mutex, 然后用 std::lock_guard<std::mutex> 封装, 或者用户 std::unique_lock<std::mutex>

std::mutex account_mutex;
int balance = 1000;

void safe_withdraw(int amount) {
    std::lock_guard<std::mutex> lock(account_mutex);  // 获取锁
    if (balance >= amount) {
        balance -= amount;  // 只有一个线程能执行这里
    }
    // lock自动释放
}
  • std::unique_lock 支持延迟上锁,条件变量,手动解锁, 比 lock_guard 慢一点

Contention锁竞争

锁竞争 (Contention):太多线程抢同一个锁会降低性能, 当持有锁时做耗时计算

应该先计算再加锁

Deadlock

必要条件

  • Mutual exclusion – Resources can be held by only one thread at a time.

  • Hold and wait – A thread holds one resource while waiting for another.

  • No preemption – A resource can’t be forcibly taken away.

  • Circular wait – Threads form a cycle of waiting for each other.

避免方法

  • 锁排序, 总是按相同顺序获取锁

  • 同时获取所有锁

void atomic_lock_example() {
    std::lock(lockA, lockB);  // 原子地获取两个锁
    std::lock_guard<std::mutex> lock1(lockA, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(lockB, std::adopt_lock);
    // 做工作...
}
  • 设置timeout
void timeout_example() {
    std::unique_lock<std::mutex> lock1(lockA);
    if (lockB.try_lock_for(std::chrono::milliseconds(100))) {
        // 成功获取两个锁
        // 做工作...
        lockB.unlock();
    } else {
        // 获取lockB超时,放弃操作
        std::cout << "无法获取所需资源,放弃操作\n";
    }
}

同步和异步

同步任务 (Synchronous):调用函数后,程序阻塞等待结果返回

异步任务 (Asynchronous) :调用函数后立即返回,任务在后台执行

Future

// 异步方式 - 非阻塞
std::future<std::string> download_file_async(const std::string& url) {
    return std::async(std::launch::async, [url]() {
        return http_get(url);  // 在后台线程中下载
    });
}

void async_example() {
    std::cout << "开始下载...\n";
    auto future = download_file_async("http://example.com/file.zip");  // 立即返回

    std::cout << "可以立即做其他事情\n";
    do_other_work();  // 同时进行其他工作

    // 需要结果时再等待
    auto data = future.get();  // 这里才等待下载完成
    std::cout << "下载完成,文件大小: " << data.size() << "\n";
}

Memory Order

在多线程编程中(尤其是 C++11 的 std::atomic),内存序(memory order) 定义了编译器和 CPU 在访问原子变量时允许怎样的重排和可见性行为。换句话说:它控制“一个线程中操作的结果什么时候、如何对其他线程可见”。

  • memory_order_relaxed 只保证“原子性”,不保证“顺序”或“可见性
  • memory_order_acquire 用于读取(load)操作,确保后续操作不会在它之前执行。我“等门打开再进去”,进门后能看到桌上的所有东西。
  • memory_order_release 用于写入(store)操作,确保之前的操作不会被移动到它之后。我把信息放在桌上,然后“开门让别人能看到”。

  • memory_order_seq_cst 所有线程都以相同顺序观察原子操作。

std::atomic

fetch add

是一个“原子读-改-写(RMW)

old = a.fetch_add(x);

语义是一个不可分割的整体:

  1. 读出当前值 old
  2. 计算 new = old + x
  3. 把 new 写回
  4. 返回 old

类似还有 fetch_sub, fetch_and, fech_or

compare exchange weak/strong

如果当前 a == expected:把 a 改成 desired,返回 true

否则:返回 false,并把 expected 改写成当前的 a 值

compare_exchange_weak:即使 a == expected,也可能返回 false(无理由失败)。

compare_exchange_strong:如果 a == expected,就必须成功(不会伪失败)。

循环重试场景:优先用 weak

不想循环 / 只尝试一次:用 strong

什么时候一定要加锁

  • 跨多个独立内存位置必须一致更新 👉 转账:要同时更新 A.balanceB.balance,这就不是一个线性化点,除非硬件支持“双地址原子提交”(一般没有)。
  • 需要“回滚/事务”的场景
  • 需要严格不暴露中间态的复合更新(除非你用额外的“提交标志/版本机制”把线性化点收敛到一个地方)

一句话:如果你找不到一个“唯一的原子提交点”,那强语义就很难无锁。

场景

Ring buffer

SPSC 模型的 Ringbuffer

  • 好处是可以读取完不等处理就读取下一个

编程练习

多线程打印与主线程等待

启动 N 个线程,每个线程打印一行 "Hello from thread X",主线程等待所有子线程结束后,再打印 "All done"

多线程求数组和

给一个很大的数组,启动 M 个线程并行计算数组和。

要求:

  • 将数组分段,每个线程负责一段。
  • 最终在主线程汇总结果。
  • 对比单线程与多线程耗时。

线程安全计数器(几乎必考)

实现一个 ThreadSafeCounter,支持:

  • increment()
  • get()

多个线程同时调用 increment(),最终结果必须正确。

要求:

  • 用互斥锁版。
  • 再实现一个用原子操作版(std::atomic / AtomicLong 等)。

生产者–消费者(用现成阻塞队列)

题目: 用语言自带的阻塞队列类(如 BlockingQueuechannel 等)实现:

  • 多个生产者线程随机生成整数,放入队列。
  • 多个消费者线程从队列取出整数并打印 / 做简单计算。
  • 跑固定时间或固定任务数后优雅退出。

考察点:

  • 阻塞队列语义(push/popput/take
  • 线程关闭协议(如何告诉消费者“没有更多任务了”)
  • 中断 / 超时等边界情况

有界阻塞队列(自己实现锁 + 条件变量)

题目: 不要用库中的阻塞队列,自己实现一个有界阻塞队列 BoundedBlockingQueue<T>

  • void put(T item):当队列满时阻塞。
  • T take():当队列空时阻塞。

内部用数组或环形缓冲区实现。

要求:

  • 使用互斥锁 + 条件变量。
  • 正确处理虚假唤醒 / spurious wakeup(循环判断条件)。

考察点(量化公司很爱问):

  • 条件变量使用模式:while (!condition) cond.wait(lock);
  • 避免丢任务 / 重复消费
  • 生产者消费者经典模型

哲学家就餐问题(死锁 & 饥饿)

题目: 实现 N 个哲学家围成一圈,每个哲学家需要拿左右两只叉子才能吃饭。用线程模拟哲学家吃饭/思考的过程。

要求:

  • 先实现一个会死锁的版本(每个人先拿左后拿右)。
  • 再修改成不会死锁的版本(如给叉子编号,先拿编号小的,再拿编号大的,或加入仲裁者)。

读写锁实现简化版读者-写者模型

题目: 实现一个线程安全的共享 map(或字典),支持:

  • get(key):只读,多线程并发调用效率要高。
  • put(key, value):写入,保证与读的正确同步。

要求:

  • 优先使用读写锁(shared_mutex / ReentrantReadWriteLock)。
  • 测试多读少写场景下的性能差异(对比普通互斥锁)。

考察点:

  • 读写锁语义
  • 读写锁 vs 普通互斥锁的优缺点
  • 读者优先 / 写者优先问题

栅栏(Barrier)/ 闭锁(Latch)

题目: 自己实现一个简单的栅栏 Barrier

  • 初始化时给定参与线程数量 N。
  • 每个线程在做完阶段性任务后调用 wait()
  • 所有线程到达后,一起继续下一阶段。

扩展:

  • 只用一次的叫 Latch(CountDownLatch)。
  • 多次循环使用的叫 Barrier(CyclicBarrier)。

考察点:

  • 如何用计数器 + 条件变量实现同步点
  • 一次性 vs 可重用屏障的区别

3.1 简单线程池(高频手写题)

题目: 自己实现一个固定大小线程池 ThreadPool

  • 构造时启动 N 个 worker 线程。
  • 提供 submit(task) 接口提交任务。
  • 内部用安全的任务队列,worker 线程从队列取任务执行。
  • 提供 shutdown() 优雅关闭线程池。

考察点(非常常见):

  • worker 线程的生命周期管理
  • 任务队列与生产者–消费者模式
  • 关闭协议设计:如何唤醒阻塞的线程并让其退出
  • 防止任务丢失/重复执行

Future / Promise(异步结果)

题目: 在线程池上实现一个返回结果的 submit

Future<R> submit(Callable<R> task);

Future 支持:

  • R get() 阻塞等待结果。
  • (可选)带超时的 get(timeout)

考察点:

  • 任务执行线程与调用 get() 的线程之间的数据同步
  • 一次性设置结果(只能 set 一次)
  • 异常传播(任务抛异常时,get() 如何处理)

并行归并排序 / 快速排序

题目: 实现并行排序:

  • 给定数组,递归地:
  • 如果区间较大:拆分成左右两半,分别用线程池提交任务并行排序。
  • 如果区间较小:用单线程排序(如 std::sort)。
  • 最后归并两个有序子数组。

考察点:

  • 任务划分粒度(granularity)与线程创建开销
  • 避免递归创建太多线程(应复用线程池)
  • 并发 vs 算法复杂度综合考虑

单生产者-单消费者无锁环形队列(SPSC Ring Buffer)

题目: 实现一个 SPSC(Single Producer, Single Consumer)环形缓冲区,用于在一个生产线程与一个消费线程之间无锁传递数据。

要求:

  • 禁用互斥锁,使用原子变量和内存序。
  • 只允许一个固定生产者线程和一个固定消费者线程。
  • buffer 满/空时的处理逻辑。

考察点(量化/高频交易非常常见):

  • cache friendly 的环形结构
  • 内存模型:memory_order_release/acquire(或语言对应语义)
  • 为什么 SPSC 可以做到 lock-free,MPSC/MPCM 难度更大

多生产者多消费者队列(MPMC)思路题

题目(可不完全实现): 讨论或尝试实现一个多生产者多消费者的高性能队列(可以参考 Disruptor / 分段锁 / Michael-Scott Queue 思路)。

可以作为设计题:

  • 不要求完全代码,可要求你说出:
  • 如何避免全局锁成为瓶颈。
  • 如何减少伪共享。
  • 如何在多核下高效进行负载均衡。

考察点:

  • 对 lock-free / wait-free 数据结构的理解
  • 常见并发队列设计中的坑(ABA、false sharing、缓存一致性流量)

市场数据撮合引擎的简化并发设计

题目: 设计并实现一个极简“撮合引擎”模型:

  • 一个线程负责接收订单(模拟外部网络输入),按序放入队列。
  • 一个撮合线程从队列中取订单、更新订单簿(如简单的价格/数量累加)。
  • 多个读线程可以查询当前订单簿快照(只读)。

要求:

  • 订单簿数据结构要线程安全。
  • 读多写少:如何提高查询效率?(例如读写锁 / 版本号快照 / RCU 思想)。

考察点:

  • 量化/交易系统常见架构
  • 写线程和读线程之间的数据发布(visibility)
  • 延迟 vs 一致性的权衡

性能调优:伪共享与缓存行对齐

题目: 写一个小程序,创建多个线程,每个线程不断累加一个数组中的不同元素,例如:

struct Data { long long x; };
Data arr[N_THREADS];
  • 测试在默认布局下的性能。
  • 再将 Data 填充 / 对齐到一个 cache line 大小(例如 64 字节),再次测试。

考察点:

  • false sharing 现象:多个线程修改同一 cache line 上不同变量导致缓存抖动
  • 如何用 padding 或对齐关键字避免伪共享
  • 量化公司关心的“纳秒级延迟”的感知

些不是编程题,但几乎一定会结合上面练习来问你:

  1. 什么是 race condition?怎么发现/避免?
  2. 死锁产生的四个必要条件是什么?你在哲学家就餐里是怎么破坏的?
  3. 互斥锁 vs 自旋锁的适用场景?在高频交易场景哪种更常见?
  4. 内存模型 / happens-before / volatile(或等价概念)是什么?
  5. 为什么要避免在锁内做 I/O 或耗时操作?
  6. 线程池的参数(核心线程数、最大线程数、队列长度)如何选?在低延迟系统有什么不同考虑?
  7. 什么是“无锁”(lock-free)和“等待自由”(wait-free)?能举例吗?

实现 lockfree Ring Buffer