一、从单线程到多线程:并发编程的进阶之路
说实话,很多C++开发者写了好几年代码,对并发编程的理解还停留在“开个线程跑任务”的阶段。我自己刚入行时也是这样——直到有一次线上服务因为锁竞争导致性能雪崩,才真正意识到并发编程的水有多深。
今天我们要聊的这几个主题,说白了就是并发编程的“进阶装备”。线程池、读写锁、future/promise、async/packaged_task,还有无锁编程——这些工具用好了,你的程序性能能上一个台阶;用不好,调试起来能让你怀疑人生。
核心要点:本章内容围绕“如何高效、安全地管理并发任务”展开。我们从线程池这个最实用的工具开始,逐步深入到同步机制和异步编程模型,最后简单聊聊无锁编程这个“高阶玩法”。
二、线程池:别再手动创建线程了
2.1 为什么需要线程池?
你想想看,每次来一个任务就 new 一个线程,任务结束就销毁——这效率能高吗?线程的创建和销毁是有开销的,频繁操作会导致性能抖动。我在一个实时数据处理项目中就踩过这个坑:每秒上千个请求,每个请求都开新线程,结果系统大部分时间都在“生娃”和“埋娃”。
线程池的核心思想很简单:提前创建一批线程,任务来了直接分配,任务结束线程不销毁,等着接下一个活。这就像餐馆里提前招好服务员,客人来了直接服务,而不是现去人才市场招人。
2.2 一个简单的线程池实现
下面这个实现我用了很多年,虽然简单但足够实用。注意看核心组件:
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
class ThreadPool {
public:
ThreadPool(size_t threads) : stop(false) {
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock, [this] {
return this->stop || !this->tasks.empty();
});
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker : workers)
worker.join();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
使用建议:线程数量一般设为 std::thread::hardware_concurrency() 的返回值。如果是CPU密集型任务,就设成这个数;如果是IO密集型,可以适当多设一些。
2.3 使用示例
int main() {
ThreadPool pool(4);
// 提交任务,获取future
auto result = pool.enqueue([](int a, int b) {
return a + b;
}, 10, 20);
std::cout << "Result: " << result.get() << std::endl;
// 批量提交
std::vector<std::future<int>> results;
for(int i = 0; i < 8; ++i) {
results.emplace_back(
pool.enqueue([i] {
return i * i;
})
);
}
for(auto &result : results)
std::cout << result.get() << ' ';
std::cout << std::endl;
return 0;
}
三、读写锁:读多写少的场景利器
3.1 shared_mutex 是什么?
传统的 std::mutex 是“排他锁”——不管读还是写,一次只能一个线程访问。但实际场景中,读操作往往远多于写操作。比如一个配置表,99%的时间都在读,只有1%的时间在更新。这时候用普通互斥锁,读线程之间互相阻塞,性能就浪费了。
读写锁(C++17 的 std::shared_mutex)解决了这个问题:多个线程可以同时读,但写的时候独占。说白了就是“读读不互斥,读写互斥,写写互斥”。
3.2 实战用法
#include <shared_mutex>
#include <unordered_map>
#include <string>
class ConfigManager {
public:
std::string get(const std::string& key) {
std::shared_lock<std::shared_mutex> lock(mutex_); // 共享锁
auto it = config_.find(key);
return it != config_.end() ? it->second : "";
}
void set(const std::string& key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(mutex_); // 独占锁
config_[key] = value;
}
private:
std::unordered_map<std::string, std::string> config_;
mutable std::shared_mutex mutex_;
};
注意:我曾经在一个项目中看到有人用 shared_mutex 保护一个只有几十个元素的 vector,结果性能反而更差了。读写锁本身有开销,只有在读操作远多于写操作(比如读:写 > 10:1)时才有优势。小数据量直接用普通 mutex 反而更快。
四、future 与 promise:线程间的“快递员”
4.1 基本概念
future 和 promise 是 C++11 引入的一对“好基友”。它们的作用是在线程之间传递数据——一个线程通过 promise 设置值,另一个线程通过 future 获取值。这就像你网购:promise 是卖家发货,future 是快递单号,你可以拿着单号随时查物流(等待结果)。
4.2 典型用法
#include <future>
#include <iostream>
#include <thread>
void worker(std::promise<int> prom) {
// 模拟耗时计算
std::this_thread::sleep_for(std::chrono::seconds(2));
prom.set_value(42); // 设置结果
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(worker, std::move(prom));
// 主线程可以干别的事
std::cout << "Waiting for result..." << std::endl;
int result = fut.get(); // 阻塞等待结果
std::cout << "Result: " << result << std::endl;
t.join();
return 0;
}
关键点:future.get() 只能调用一次!多次调用会抛出异常。如果你需要多个线程等待同一个结果,用 std::shared_future。
五、async 与 packaged_task:更优雅的异步编程
5.1 std::async:一键异步
std::async 是 future/promise 的“高级封装”。你只需要告诉它“我要执行这个函数”,它自动帮你创建线程(或者在线程池中执行),然后返回一个 future。
#include <future>
#include <iostream>
int compute(int x) {
return x * x;
}
int main() {
// 默认策略:可能异步,也可能同步
auto fut1 = std::async(compute, 10);
// 强制异步执行
auto fut2 = std::async(std::launch::async, compute, 20);
// 延迟执行(直到调用 get 才执行)
auto fut3 = std::async(std::launch::deferred, compute, 30);
std::cout << fut1.get() << " "
<< fut2.get() << " "
<< fut3.get() << std::endl;
return 0;
}
我的经验:std::async 默认策略(std::launch::async | std::launch::deferred)有个坑——它可能不创建新线程,而是在调用 get() 时同步执行。如果你明确需要异步,请指定 std::launch::async。
5.2 packaged_task:把函数打包成“任务包”
packaged_task 比 async 更底层一些。它把函数和 future 打包在一起,你可以把这个“任务包”交给线程池或者某个线程去执行。
#include <future>
#include <iostream>
#include <thread>
int main() {
// 创建一个打包任务
std::packaged_task<int(int, int)> task([](int a, int b) {
return a + b;
});
// 获取 future
std::future<int> fut = task.get_future();
// 在另一个线程执行
std::thread t(std::move(task), 10, 20);
std::cout << "Result: " << fut.get() << std::endl;
t.join();
return 0;
}
六、无锁编程简介:高性能的“双刃剑”
6.1 为什么需要无锁?
锁虽然好用,但有两个问题:一是可能死锁,二是高并发下锁竞争会导致性能下降。无锁编程(Lock-Free)通过原子操作和 CAS(Compare-And-Swap)来实现线程安全,避免了锁的开销。
6.2 一个简单的无锁栈
#include <atomic>
template<typename T>
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(nullptr) {}
};
std::atomic<Node*> head_;
public:
LockFreeStack() : head_(nullptr) {}
void push(const T& value) {
Node* new_node = new Node(value);
new_node->next = head_.load(std::memory_order_relaxed);
while(!head_.compare_exchange_weak(
new_node->next,
new_node,
std::memory_order_release,
std::memory_order_relaxed
));
}
bool pop(T& value) {
Node* old_head = head_.load(std::memory_order_relaxed);
while(old_head != nullptr) {
if(head_.compare_exchange_weak(
old_head,
old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed
)) {
value = old_head->data;
delete old_head;
return true;
}
}
return false;
}
};
警告:无锁编程非常容易出错。我见过一个团队花了两周时间调试一个无锁队列的 ABA 问题。除非你是并发编程专家,否则建议优先使用锁和标准库提供的并发容器。无锁编程适合的场景:性能要求极高、且你能证明锁是瓶颈的时候。
七、知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
八、总结与避坑指南
聊了这么多,最后分享几个我踩过的坑:
- 线程池大小不是越大越好——我曾经把线程池设成100个线程,结果CPU上下文切换开销比任务执行时间还长。对于CPU密集型任务,线程数等于核心数就够了。
- shared_mutex 不是万能药——如果写操作频繁,读写锁可能比普通互斥锁还慢。我在一个实时日志系统中就吃过这个亏。
- future.get() 会阻塞当前线程——如果你在UI线程中调用,界面会卡死。考虑用 then() 或者回调的方式处理结果。
- 无锁编程慎用——除非你能用性能分析工具证明锁是瓶颈,否则别碰。我见过太多“优化”成无锁后反而更慢的例子。
记住一句话:先保证正确,再追求性能。并发编程的调试难度是指数级上升的,能用简单方案就别炫技。