一、为什么会有资源竞争问题
想象一下,你和朋友同时想用同一支笔签字,但笔只有一支,这时候就会产生争抢。在多线程编程中,类似的情况比比皆是。比如多个线程同时读写同一个变量,或者操作同一个文件,如果没有合理的协调机制,程序就可能出现数据错乱、崩溃等不可预知的问题。
C++作为一门贴近硬件的语言,对多线程的支持非常灵活,但这也意味着开发者需要自己处理很多细节。下面我们通过一个简单的例子来看看资源竞争是怎么发生的:
// 技术栈:C++11及以上版本
#include <iostream>
#include <thread>
int shared_data = 0; // 共享数据
void increment() {
for (int i = 0; i < 100000; ++i) {
shared_data++; // 多个线程同时修改shared_data
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << shared_data << std::endl;
return 0;
}
运行这段代码,你可能会发现shared_data的最终值并不是预期的200000,而是一个比它小的随机数。这就是典型的资源竞争问题——两个线程同时读取、修改同一个变量,导致部分修改丢失。
二、解决资源竞争的常见方法
1. 互斥锁(Mutex)
互斥锁就像厕所的门锁,一个人进去后锁上门,其他人就得在外面等着。C++标准库提供了std::mutex来实现这个功能:
#include <mutex>
std::mutex mtx; // 全局互斥锁
void safe_increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // 加锁
shared_data++; // 临界区代码
mtx.unlock(); // 解锁
}
}
不过直接使用lock()和unlock()容易忘记解锁,更推荐用std::lock_guard自动管理锁的生命周期:
void safer_increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时自动解锁
shared_data++;
}
}
2. 原子操作
对于简单的数据类型,C++11提供了原子类型std::atomic,它通过硬件指令保证操作的原子性:
#include <atomic>
std::atomic<int> atomic_data(0); // 原子整型
void atomic_increment() {
for (int i = 0; i < 100000; ++i) {
atomic_data++; // 原子操作,无需加锁
}
}
原子操作的性能通常比互斥锁更高,但仅适用于基本数据类型。
3. 读写锁(Read-Write Lock)
当读操作远多于写操作时,可以使用std::shared_mutex(C++17引入):
#include <shared_mutex>
std::shared_mutex rw_mutex;
int read_heavy_data = 42;
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_mutex); // 共享锁(多读)
std::cout << read_heavy_data << std::endl;
}
void writer() {
std::unique_lock<std::shared_mutex> lock(rw_mutex); // 独占锁(单写)
read_heavy_data++;
}
三、进阶技巧与陷阱规避
1. 避免死锁
当多个锁以不同顺序加锁时,可能会产生死锁。例如:
// 错误示范:可能死锁
void thread1() {
mtx1.lock();
mtx2.lock();
// ...
mtx2.unlock();
mtx1.unlock();
}
void thread2() {
mtx2.lock();
mtx1.lock();
// ...
mtx1.unlock();
mtx2.unlock();
}
解决方案是使用std::lock()一次性锁定多个互斥量:
// 正确做法
void safe_thread() {
std::lock(mtx1, mtx2); // 同时锁定(避免死锁)
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// ...
}
2. 条件变量
当线程需要等待某个条件时,可以用std::condition_variable:
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;
void waiting_thread() {
std::unique_lock<std::mutex> lock(cv_mtx);
cv.wait(lock, []{ return ready; }); // 等待ready为true
std::cout << "条件满足!" << std::endl;
}
void notifying_thread() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(cv_mtx);
ready = true;
}
cv.notify_one(); // 通知等待线程
}
四、应用场景与选型建议
- 高频计数器:优先选择原子操作(如
std::atomic) - 配置热更新:读写锁(
std::shared_mutex) - 任务队列:互斥锁+条件变量组合
- 性能敏感场景:考虑无锁数据结构(如
folly::AtomicHashMap)
需要注意的是,锁的粒度不宜过大——锁住整个数据库操作显然不如只锁住关键变量高效。另外,尽量使用RAII(资源获取即初始化)风格的锁管理类,避免手动解锁的遗漏。
总结来说,C++多线程编程就像指挥一个交响乐团,既要让各个声部(线程)自由发挥,又要确保整体和谐。掌握这些同步机制,你就能写出既高效又可靠的多线程程序。
评论