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);
语义是一个不可分割的整体:
- 读出当前值 old
- 计算 new = old + x
- 把 new 写回
- 返回 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.balance和B.balance,这就不是一个线性化点,除非硬件支持“双地址原子提交”(一般没有)。 - 需要“回滚/事务”的场景
- 需要严格不暴露中间态的复合更新(除非你用额外的“提交标志/版本机制”把线性化点收敛到一个地方)
一句话:如果你找不到一个“唯一的原子提交点”,那强语义就很难无锁。
场景
Ring buffer
SPSC 模型的 Ringbuffer
- 好处是可以读取完不等处理就读取下一个
编程练习
多线程打印与主线程等待
启动 N 个线程,每个线程打印一行 "Hello from thread X",主线程等待所有子线程结束后,再打印 "All done"。
多线程求数组和
给一个很大的数组,启动 M 个线程并行计算数组和。
要求:
- 将数组分段,每个线程负责一段。
- 最终在主线程汇总结果。
- 对比单线程与多线程耗时。
线程安全计数器(几乎必考)
实现一个 ThreadSafeCounter,支持:
increment()get()
多个线程同时调用 increment(),最终结果必须正确。
要求:
- 用互斥锁版。
- 再实现一个用原子操作版(
std::atomic/AtomicLong等)。
生产者–消费者(用现成阻塞队列)
题目:
用语言自带的阻塞队列类(如 BlockingQueue、channel 等)实现:
- 多个生产者线程随机生成整数,放入队列。
- 多个消费者线程从队列取出整数并打印 / 做简单计算。
- 跑固定时间或固定任务数后优雅退出。
考察点:
- 阻塞队列语义(
push/pop,put/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 或对齐关键字避免伪共享
- 量化公司关心的“纳秒级延迟”的感知
些不是编程题,但几乎一定会结合上面练习来问你:
- 什么是 race condition?怎么发现/避免?
- 死锁产生的四个必要条件是什么?你在哲学家就餐里是怎么破坏的?
- 互斥锁 vs 自旋锁的适用场景?在高频交易场景哪种更常见?
- 内存模型 / happens-before / volatile(或等价概念)是什么?
- 为什么要避免在锁内做 I/O 或耗时操作?
- 线程池的参数(核心线程数、最大线程数、队列长度)如何选?在低延迟系统有什么不同考虑?
- 什么是“无锁”(lock-free)和“等待自由”(wait-free)?能举例吗?
实现 lockfree Ring Buffer