5. 线程与同步机制:std::thread、std::mutex、std::condition_variable 的跨平台封装
多线程编程,说白了就是让程序同时干好几件事。但这件事做起来,坑比想象的多。我记得刚入行那会儿,在 Windows 上用 CreateThread,到了 Linux 上又得换成 pthread_create,光适配平台就够头疼的。后来 C++11 推出了 std::thread,总算有了统一标准。但实际项目里,直接裸用标准库还是不够——你想想看,线程池、超时控制、异常安全,这些标准库都没直接给。
所以这一章,我带你从实战角度出发,把 std::thread、std::mutex、std::condition_variable 封装成一套跨平台、可复用的工具。嗯,这里要注意:封装不是包一层就完事,而是要解决真实项目中的痛点。
5.1 为什么需要封装?
直接使用标准库有什么问题?我列几个亲身踩过的坑:
- 资源泄漏:std::thread 如果没 join 或 detach,析构时会直接 terminate。我在项目中见过好几次,线程跑着跑着程序就崩了,查了半天才发现是线程对象销毁时没处理。
- 异常安全:mutex 加锁后如果抛出异常,锁就永远解不开了。标准库没有自动解锁的机制,你得自己用 RAII 包装。
- 平台差异:虽然 std::thread 是跨平台的,但线程优先级、CPU 亲和性、线程命名这些功能,标准库都没提供。你得自己封装平台 API。
- 易用性:condition_variable 的 wait 经常需要配合 while 循环检查条件,写起来啰嗦还容易出错。
核心思路:封装的目标不是替代标准库,而是在标准库之上提供更安全、更易用、更强大的接口。说白了,就是让调用者少写代码,少犯错。
5.2 线程封装:从 std::thread 到 Thread 类
我习惯把线程封装成一个类,叫 Thread。它内部持有 std::thread,但提供了更安全的生命周期管理。
class Thread {
public:
// 启动线程,传入可调用对象
template<typename Func, typename... Args>
explicit Thread(Func&& func, Args&&... args)
: m_thread(std::forward<Func>(func), std::forward<Args>(args)...)
, m_joinable(true) {}
// 析构时自动 join,避免 terminate
~Thread() {
if (m_joinable && m_thread.joinable()) {
m_thread.join();
}
}
// 禁止拷贝,允许移动
Thread(const Thread&) = delete;
Thread& operator=(const Thread&) = delete;
Thread(Thread&& other) noexcept : m_thread(std::move(other.m_thread)) {
other.m_joinable = false;
}
void join() {
if (m_joinable && m_thread.joinable()) {
m_thread.join();
m_joinable = false;
}
}
void detach() {
if (m_joinable && m_thread.joinable()) {
m_thread.detach();
m_joinable = false;
}
}
// 获取线程 ID
std::thread::id get_id() const { return m_thread.get_id(); }
private:
std::thread m_thread;
bool m_joinable = false;
};
我的经验:析构函数里自动 join 这个设计,曾经救过我一命。有一次在异常处理路径上忘了 join 线程,程序直接崩了。加上这个封装后,再也没出过类似问题。不过要注意:如果线程死循环,析构时会卡住,所以最好配合超时机制。
5.3 互斥锁封装:RAII 与超时锁
std::mutex 本身是好的,但 std::lock_guard 和 std::unique_lock 的用法容易混淆。我封装了一个更直观的 LockGuard,同时支持超时。
class Mutex {
public:
Mutex() = default;
~Mutex() = default;
void lock() { m_mutex.lock(); }
void unlock() { m_mutex.unlock(); }
// 尝试加锁,支持超时
bool try_lock_for(int timeout_ms) {
std::unique_lock<std::mutex> lock(m_mutex, std::defer_lock);
if (timeout_ms < 0) {
lock.lock();
return true;
}
// 使用 timed_mutex 或 condition_variable 实现超时
// 这里简化处理,实际项目中建议用 std::timed_mutex
return lock.try_lock_for(std::chrono::milliseconds(timeout_ms));
}
private:
std::mutex m_mutex;
};
// RAII 封装
class LockGuard {
public:
explicit LockGuard(Mutex& mutex) : m_mutex(mutex) {
m_mutex.lock();
}
~LockGuard() {
m_mutex.unlock();
}
private:
Mutex& m_mutex;
};
避坑指南:我曾经在项目中直接用 std::lock_guard,结果在锁保护区域内调用了回调函数,回调里又尝试加同一把锁——死锁了。后来我改用 std::recursive_mutex 解决了,但更好的做法是设计成不允许重入,并在文档里明确说明。
5.4 条件变量封装:更安全的等待
std::condition_variable 的 wait 需要配合 unique_lock 和 while 循环,写起来很繁琐。我封装了一个 Condition 类,把常见模式简化成一行调用。
class Condition {
public:
Condition() = default;
// 等待条件成立,支持超时
template<typename Predicate>
bool wait_for(Mutex& mutex, int timeout_ms, Predicate pred) {
std::unique_lock<std::mutex> lock(m_mutex);
if (timeout_ms < 0) {
m_cv.wait(lock, std::move(pred));
return true;
}
return m_cv.wait_for(lock,
std::chrono::milliseconds(timeout_ms),
std::move(pred));
}
// 通知一个等待线程
void notify_one() { m_cv.notify_one(); }
// 通知所有等待线程
void notify_all() { m_cv.notify_all(); }
private:
std::condition_variable m_cv;
std::mutex m_mutex; // 内部锁,用于保护条件变量自身
};
使用示例:
Mutex mutex;
Condition cond;
bool ready = false;
// 生产者线程
void producer() {
LockGuard lock(mutex);
ready = true;
cond.notify_one();
}
// 消费者线程
void consumer() {
cond.wait_for(mutex, 5000, [&]() { return ready; });
// 如果 5 秒内 ready 变为 true,继续执行;否则超时返回
}
5.5 跨平台封装:线程优先级与命名
标准库不提供线程优先级和命名功能,但实际项目中经常需要。我封装了平台相关的实现:
class ThreadUtil {
public:
// 设置线程名称(调试时很有用)
static void set_name(const std::string& name) {
#ifdef _WIN32
// Windows: 使用 SetThreadDescription
std::wstring wname(name.begin(), name.end());
SetThreadDescription(GetCurrentThread(), wname.c_str());
#elif defined(__linux__)
// Linux: 使用 prctl
prctl(PR_SET_NAME, name.c_str(), 0, 0, 0);
#elif defined(__APPLE__)
// macOS: 使用 pthread_setname_np
pthread_setname_np(name.c_str());
#endif
}
// 设置线程优先级(0-99,99 最高)
static bool set_priority(int priority) {
#ifdef _WIN32
int win_priority = THREAD_PRIORITY_NORMAL;
if (priority > 80) win_priority = THREAD_PRIORITY_HIGHEST;
else if (priority > 60) win_priority = THREAD_PRIORITY_ABOVE_NORMAL;
else if (priority < 20) win_priority = THREAD_PRIORITY_LOWEST;
return SetThreadPriority(GetCurrentThread(), win_priority) != 0;
#elif defined(__linux__)
// Linux: 使用 pthread_setschedparam
struct sched_param param;
param.sched_priority = priority;
return pthread_setschedparam(pthread_self(), SCHED_OTHER, ¶m) == 0;
#else
// macOS 对优先级支持有限,直接返回 true
return true;
#endif
}
};
我的建议:线程优先级是个双刃剑。我在一个音视频项目中把解码线程优先级设得过高,结果 UI 线程卡顿严重。后来我改用线程池 + 任务优先级,效果反而更好。所以,除非你非常清楚自己在做什么,否则别轻易动优先级。
5.6 知识体系总览
下面这张图总结了本章的核心封装逻辑:
5.7 实战中的注意事项
封装完了,但真正用好还需要注意几点:
- 避免死锁:多把锁时,始终按相同顺序加锁。我习惯用 std::lock 同时锁多个 mutex,或者封装一个 ScopedLock 类来管理多锁。
- 条件变量的虚假唤醒:wait 返回后,条件不一定成立。所以一定要用 while 循环检查谓词,而不是 if。我的封装里已经帮你做了这件事。
- 性能考量:锁的粒度要适中。太粗了并发度低,太细了锁开销大。我在一个网络库中曾经把锁拆到每个连接上,性能提升了 3 倍。
- 调试支持:给线程起个有意义的名字,崩溃时能快速定位。我习惯在构造函数里自动调用 set_name。
我曾经踩过的坑:有一次在析构函数里加锁,结果锁被另一个线程持有着,析构线程直接卡死。后来我改用 try_lock_for 加超时,超时后打印警告日志,问题才暴露出来。所以,析构函数里尽量别加锁,如果非要加,一定要有超时机制。
好了,这一章的内容就到这里。线程封装看似简单,但每个细节都关系到程序的稳定性和性能。希望这些封装能帮你少走弯路,写出更健壮的多线程代码。