在操作系统层面,进程和线程是两种不同的执行单元。理解它们的区别是多线程编程的基础。
进程就像是一个独立的应用程序实例,它拥有:
而线程则是进程内的执行单元:
实际开发经验:在Linux系统中,可以通过
ps -eLf命令查看进程和线程的关系。主线程的PID和LWP(轻量级进程ID)相同,子线程的LWP则不同。
并发(Concurrency)和并行(Parallelism)这两个概念在实际开发中经常被混淆:
cpp复制// 并发示例:单核CPU上的线程切换
void task1() { /* 执行任务1 */ }
void task2() { /* 执行任务2 */ }
std::thread t1(task1);
std::thread t2(task2);
// 在单核CPU上,t1和t2会交替执行
cpp复制// 并行示例:多核CPU上的真正并行
std::vector<std::thread> threads;
for(int i=0; i<4; ++i) {
threads.emplace_back([]{
// 每个线程可能在不同核心上并行执行
});
}
性能调优技巧:使用
std::thread::hardware_concurrency()获取硬件支持的线程数,避免创建过多线程导致性能下降。
多线程编程的主要难点在于共享数据的管理。以下是三个典型问题及其解决方案:
当多个线程同时访问共享数据,且至少有一个线程在修改数据时,最终结果依赖于线程执行的时序。
cpp复制int counter = 0;
void increment() {
for(int i=0; i<100000; ++i) {
counter++; // 这不是原子操作!
}
}
std::thread t1(increment);
std::thread t2(increment);
// 最终counter可能小于200000
C++标准明确定义的未定义行为:一个线程写数据,同时另一个线程读或写同一数据,且没有同步机制。
多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。
cpp复制std::mutex mtx1, mtx2;
void thread1() {
mtx1.lock();
mtx2.lock(); // 如果thread2先拿到mtx2,这里会死锁
// ...
mtx2.unlock();
mtx1.unlock();
}
void thread2() {
mtx2.lock();
mtx1.lock(); // 如果thread1先拿到mtx1,这里会死锁
// ...
mtx1.unlock();
mtx2.unlock();
}
调试技巧:在Linux下可以使用
gdb的thread apply all bt命令查看所有线程的调用栈,帮助定位死锁位置。
Lambda表达式是现代C++多线程编程的首选方式,它可以直接捕获上下文变量,比普通函数更灵活。
cpp复制int main() {
int local_var = 42;
// 按值捕获
std::thread t1([=] {
std::cout << local_var << std::endl; // 安全,使用的是副本
});
// 按引用捕获(危险!)
std::thread t2([&] {
local_var++; // 可能引发数据竞争
});
t1.join();
t2.join();
return 0;
}
最佳实践:优先使用值捕获(
[=]),除非确实需要修改外部变量。使用引用捕获时,必须确保同步机制。
智能指针在多线程环境中的使用需要特别注意:
std::unique_ptr:所有权唯一,不能共享,适合线程间转移数据std::shared_ptr:引用计数线程安全,但指向的对象访问仍需同步std::weak_ptr:解决循环引用问题,不影响引用计数cpp复制void thread_func(std::shared_ptr<int> ptr) {
// 即使引用计数操作是原子的,访问数据仍需加锁
std::lock_guard<std::mutex> lock(some_mutex);
*ptr += 1;
}
int main() {
auto ptr = std::make_shared<int>(0);
std::thread t(thread_func, ptr);
t.join();
return 0;
}
性能提示:
std::make_shared比直接new更高效,因为它一次性分配内存存储对象和控制块。
多线程间传递大数据时,移动语义可以避免不必要的拷贝:
cpp复制void process_data(std::vector<int>&& data) {
// 处理数据
}
int main() {
std::vector<int> big_data(1000000, 42);
std::thread t(process_data, std::move(big_data));
t.join();
// 此时big_data已被移动,不应再使用
return 0;
}
注意事项:被移动的对象处于有效但未定义状态,只能进行析构或重新赋值操作。
互斥锁是最基础的同步原语,但直接使用容易出错:
cpp复制std::mutex mtx;
int shared_data = 0;
void unsafe_increment() {
mtx.lock();
// 如果这里抛出异常,锁永远不会释放!
shared_data++;
mtx.unlock();
}
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx); // RAII风格
shared_data++; // 异常安全
}
经验法则:永远使用
std::lock_guard或std::unique_lock,避免直接调用lock()/unlock()。
条件变量用于线程间通知,使用时必须注意虚假唤醒问题:
cpp复制std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
std::queue<int> data_queue;
void producer() {
for(int i=0; i<10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
data_ready = true;
}
cv.notify_one();
}
}
void consumer() {
while(true) {
std::unique_lock<std::mutex> lock(mtx);
// 必须使用while循环检查条件
cv.wait(lock, []{ return data_ready; });
while(!data_queue.empty()) {
int data = data_queue.front();
data_queue.pop();
std::cout << data << std::endl;
}
data_ready = false;
}
}
调试技巧:在复杂条件变量场景中,可以添加日志输出条件变量的状态变化,便于排查问题。
C++17引入的std::shared_mutex在读取多、写入少的场景下能显著提升性能:
cpp复制class ThreadSafeConfig {
std::shared_mutex rw_mutex;
std::unordered_map<std::string, std::string> config;
public:
std::string get(const std::string& key) {
std::shared_lock<std::shared_mutex> lock(rw_mutex); // 读锁
return config[key];
}
void set(const std::string& key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(rw_mutex); // 写锁
config[key] = value;
}
};
性能对比:在8核机器上测试,对于读多写少(90%读/10%写)的场景,
shared_mutex比普通mutex吞吐量可提升3-5倍。
虽然C++标准库没有直接提供线程池,但我们可以自己实现一个基础版本:
cpp复制class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop = false;
public:
ThreadPool(size_t threads) {
for(size_t i=0; i<threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock,
[this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(auto& worker : workers)
worker.join();
}
};
生产环境建议:考虑使用成熟的第三方库如Intel TBB或微软PPL,它们提供了更完善的线程池实现。
C++提供了多种异步编程方式,各有适用场景:
| 方式 | 适用场景 | 特点 |
|---|---|---|
| std::thread | 需要精细控制线程生命周期 | 底层,需要手动管理 |
| std::async | 简单的异步任务 | 自动管理线程,但开销较大 |
| std::promise | 需要从线程获取返回值 | 线程间结果传递 |
| 线程池 | 大量短任务 | 减少线程创建销毁开销 |
cpp复制// std::async示例
auto future = std::async(std::launch::async, []{
std::this_thread::sleep_for(1s);
return 42;
});
// 可以做其他工作...
std::cout << future.get() << std::endl; // 阻塞直到结果就绪
性能注意:默认策略的
std::async可能不会立即创建新线程,如果需要确保异步执行,使用std::launch::async策略。
死锁是多线程编程中最棘手的问题之一。以下是典型死锁模式:
cpp复制// 线程1
lock(A);
lock(B);
// 线程2
lock(B);
lock(A);
解决方案:统一加锁顺序,或使用std::lock同时加锁。
cpp复制std::mutex m;
void foo() {
m.lock();
bar(); // 内部又尝试lock()
m.unlock();
}
解决方案:使用递归锁std::recursive_mutex,或重构代码避免重复加锁。
调试工具推荐:
- Linux:
helgrind(Valgrind工具之一)- Windows: Visual Studio的并发分析工具
- 跨平台: Clang ThreadSanitizer (TSAN)
多线程程序的性能问题通常出现在:
锁竞争:太多线程争抢同一把锁
缓存失效:频繁修改的共享数据导致CPU缓存失效
虚假共享:不同CPU核心修改同一缓存行的不同变量
cpp复制struct alignas(64) CacheLineAligned {
int data1; // 单独占用一个缓存行
};
CacheLineAligned var1, var2; // 不会产生虚假共享
性能分析工具:
- Linux:
perf,Intel VTune- Windows:
Windows Performance Analyzer- 跨平台:
Google Benchmark用于微基准测试
C++20引入了std::jthread,相比std::thread有两个主要改进:
cpp复制void worker(std::stop_token stoken) {
while(!stoken.stop_requested()) {
// 执行工作...
}
}
int main() {
std::jthread jt(worker); // 不需要手动join
// ...
jt.request_stop(); // 请求线程停止
return 0;
} // jt析构时会自动join
C++20为原子变量添加了等待/通知机制,类似于条件变量但更轻量:
cpp复制std::atomic<bool> ready{false};
void consumer() {
ready.wait(false); // 阻塞直到ready变为true
// 处理数据...
}
void producer() {
// 准备数据...
ready.store(true);
ready.notify_one(); // 唤醒等待的线程
}
性能优势:相比条件变量,原子等待通常有更低的延迟和更高的吞吐量。
thread_local变量每个线程有独立副本cpp复制// 线程安全队列的基本实现
template<typename T>
class ConcurrentQueue {
std::queue<T> queue;
mutable std::mutex mtx;
std::condition_variable cv;
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(std::move(value));
cv.notify_one();
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mtx);
if(queue.empty()) return false;
value = std::move(queue.front());
queue.pop();
return true;
}
void wait_and_pop(T& value) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]{ return !queue.empty(); });
value = std::move(queue.front());
queue.pop();
}
};
静态变量初始化竞争:
双重检查锁定问题:
cpp复制if(!ptr) { // 第一次检查
lock_guard lock(mtx);
if(!ptr) { // 第二次检查
ptr = new T();
}
}
std::call_once或原子变量信号处理中的锁:
代码审查要点:在多线程代码审查时,特别关注所有共享数据的访问点,确保都有适当的同步机制。
无锁(lock-free)数据结构可以避免锁带来的性能问题,但实现复杂:
cpp复制template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head = nullptr;
public:
void push(const T& data) {
Node* new_node = new Node{data, nullptr};
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node)) {
// CAS失败,重试
}
}
bool pop(T& result) {
Node* old_head = head.load();
while(old_head &&
!head.compare_exchange_weak(old_head, old_head->next)) {
// CAS失败,重试
}
if(!old_head) return false;
result = old_head->data;
delete old_head;
return true;
}
};
注意事项:无锁不等于等待无关(wait-free),且正确实现极其困难,建议优先使用成熟的库实现。
C++内存模型定义了原子操作的内存顺序语义:
memory_order_relaxed:无顺序保证,仅保证原子性memory_order_consume:依赖关系顺序memory_order_acquire:获取操作,保证之后的读操作不会被重排到它前面memory_order_release:释放操作,保证之前的写操作不会被重排到它后面memory_order_acq_rel:获取-释放,同时具有acquire和release语义memory_order_seq_cst:顺序一致性,默认最严格模式cpp复制std::atomic<bool> x{false}, y{false};
int data = 0;
void thread1() {
data = 42; // 1
x.store(true, std::memory_order_release); // 2
}
void thread2() {
while(!x.load(std::memory_order_acquire)); // 3
if(data == 42) { // 4
y.store(true, std::memory_order_relaxed);
}
}
专家建议:除非是性能关键路径且完全理解内存模型,否则使用默认的
memory_order_seq_cst。
不同操作系统对线程的支持有差异:
| 特性 | Windows | Linux/POSIX |
|---|---|---|
| 线程API | CreateThread | pthread_create |
| 线程本地存储 | __declspec(thread) | __thread或pthread_key |
| 原子操作 | Interlocked系列 | __atomic内置 |
| 纤程/协程 | Fiber | ucontext |
可移植性技巧:始终使用C++标准库的线程设施(
std::thread等),避免直接调用平台特定API。
Intel TBB (Threading Building Blocks):
Microsoft PPL (Parallel Patterns Library):
Boost.Thread:
cpp复制// Intel TBB示例
#include <tbb/parallel_for.h>
void parallel_work() {
tbb::parallel_for(0, 100, [](int i) {
// 并行处理
});
}
C++20引入了协程支持,为异步编程提供了新范式:
cpp复制#include <coroutine>
task<int> async_compute() {
int result = co_await async_operation();
co_return result;
}
学习建议:协程是高级主题,建议先掌握基础多线程编程再学习。
C++17引入了并行算法,可以轻松利用多核:
cpp复制#include <algorithm>
#include <execution>
void parallel_sort() {
std::vector<int> data = {...};
std::sort(std::execution::par, data.begin(), data.end());
}
性能提示:对于小数据集,并行算法可能因启动开销而比串行版本慢。
在实际项目中应用多线程技术时,我总结出以下几点经验:
从简单设计开始:先实现正确性,再优化性能。过度设计的多线程代码难以维护。
测试至关重要:多线程bug往往难以复现,需要:
性能分析驱动优化:不要猜测瓶颈所在,使用profiler定位热点。
文档同步假设:明确记录哪些函数是线程安全的,哪些需要外部同步。
逐步复杂化:先实现无共享数据的并行,再引入必要的共享和同步。
一个真实案例:在实现高并发网络服务时,最初使用每连接一线程模型,在连接数超过5000时性能急剧下降。后来改用I/O多路复用+线程池,性能提升10倍以上。关键教训:线程不是越多越好,需要根据工作负载特点选择合适模型。