C++11 并发支持库上
一、Thread库

thread库中分有两个重量级嘉宾,一个是核心类thread;一个是命名空间this_thread
std::thread 用于“管理别人”(创建和控制其他线程),而 std::this_thread 用于“了解自己”(操作当前正在执行的线程)
| 特性 | std::thread (类) | std::this_thread (命名空间) |
|---|---|---|
| 操作对象 | 其他线程 (由该对象管理的线程) | 当前执行的线程 (调用函数所在的线程) |
| 身份 | 一个句柄,用于控制和管理一个特定的执行流。 | 一个引用,指向当前正在运行的执行流,没有独立的句柄对象。 |
| 主要职责 | 创建、等待 (join)、分离 (detach)、转移所有权 (move)、获取被管理线程的ID。 | 获取当前线程ID (get_id)、让出CPU (yield)、休眠 (sleep_for, sleep_until)。 |
| 能否用于自身? | 不能。一个 std::thread 对象无法直接操作创建它的那个线程。例如,无法调用 t.join() 来等待自身。 | 能。这是它的专长,专门用来查询和控制当前代码所在的线程。 |
thread类
thread - C++ Reference (cplusplus.com)

构造

#include <utility>
#include <functional>
namespace std {
// 线程核心类:轻量级原生线程句柄封装
class thread {
public:
// 线程ID类型:平台无关的唯一标识
class id;
// 原生线程句柄类型:平台相关(Linux:pthread_t/Windows:HANDLE)
using native_handle_type = /* 平台原生线程句柄 */;
// 1. 构造函数
thread() noexcept; // 默认构造:空线程,不创建OS线程
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args); // 带任务构造:创建OS线程
// 2. 禁用拷贝(线程不可拷贝,符合OS线程语义)
thread(const thread&) = delete;
thread& operator=(const thread&) = delete;
// 3. 支持移动语义(线程所有权转移)
thread(thread&& other) noexcept;
thread& operator=(thread&& other) noexcept;
// 4. 析构函数
~thread();
// 5. 核心成员函数
void join(); // 等待线程执行完成(阻塞)
void detach(); // 分离线程:后台运行,自动释放资源
bool joinable() const noexcept; // 判断线程是否可等待/分离
id get_id() const noexcept; // 获取线程唯一ID
native_handle_type native_handle();// 获取平台原生线程句柄
static unsigned int hardware_concurrency() noexcept; // 获取CPU核心数
private:
// 核心私有成员:仅两个关键变量!
native_handle_type _M_thread; // 平台原生线程句柄(核心标识)
id _M_id; // 线程唯一ID(平台无关)
};
// 线程ID类:轻量级标识,仅存储一个数值
class thread::id {
private:
unsigned long long _M_thread_id; // 线程ID数值(全局唯一)
friend class thread;
public:
bool operator==(id) const noexcept;
// 其他比较运算符、哈希支持...
};
}
可变参数模板构造函数
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
-
立即启动新线程
-
f可以是:函数指针、lambda、函数对象、std::function -
args是传递给f的参数
void func(int a, double b, const std::string& s) {}
// 基本用法
std::thread t1(func, 42, 3.14, "hello");
// lambda
std::thread t2([](int x) { return x * x; }, 10);
// 成员函数
struct Foo {
void bar(int x) {}
};
Foo foo;
std::thread t3(&Foo::bar, &foo, 42); // 传入对象指针
参数传递的关键细节:
| 参数类型 | 默认行为 | 需要处理的情况 |
|---|---|---|
| 值类型 | 拷贝传递 | 直接传值 |
| 引用类型 | 按值拷贝,无法传递引用! | 用 std::ref / std::cref |
| 移动-only类型 | 无法拷贝 | 用 std::move |
void modify(int& x) { x++; } // 需要引用
void accept(std::unique_ptr<int> p) {} // 移动-only
int n = 0;
std::unique_ptr<int> ptr(new int(42));
// 传递引用:必须用 std::ref
std::thread t1(modify, std::ref(n));
// 传递移动-only对象:必须用 std::move
std::thread t2(accept, std::move(ptr));
t1.join();
t2.join();
// n == 1, ptr 已被移空
需要注意的是,在OS中每一个线程都具备唯一性(独立的PID、栈……)故无法说将一个线程拷贝,且很容易导致资源管理混乱,故thread禁止了拷贝构造。
但是,thread类保留了移动构造,允许线程的管理权从一个对象交给另一个对象。保证了灵活性。
| 操作 | 含义 | 对 thread 意味着 | 安全吗? |
|---|---|---|---|
| 拷贝 | 复制一份一模一样的 | 两个对象管同一个线程 | 危险 |
| 移动 | 把所有权移交过去 | 只有新对象管线程,旧对象变空 | 安全 |
std::thread t1(func);
std::thread t2(std::move(t1)); // t2 接管线程
// t1 现在是空壳
t2.join();
成员函数
void join()
-
阻塞当前线程,等待
*this关联的线程执行完毕 -
调用后
joinable() == false -
要求调用前
joinable() == true,否则抛出std::system_error
std::thread t([]{ std::this_thread::sleep_for(std::chrono::seconds(1)); });
t.join(); // 主线程等待1秒
// t.joinable() == false
// t.join(); // 错误!重复调用会抛异常
void detach()
-
将线程与
thread对象分离,线程在后台独立运行 -
调用后
joinable() == false -
线程结束后资源由运行时自动回收
-
危险:主线程结束时进程终止,detach 的线程也会被强制终止
std::thread t([]{
// 这个线程会在后台独立运行
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "This may never print if main ends early" << std::endl;
});
t.detach();
// 主线程继续执行,可能先于子线程结束
bool joinable() const noexcept;
-
返回
true的条件:线程正在运行(未被 join 也未被 detach)
std::thread::id get_id() const noexcept;
-
返回线程的唯一标识符
-
默认构造的对象返回
std::thread::id()
void swap(thread& other) noexcept;
-
交换两个
thread对象关联的线程所有权
std::thread t1(func1);
std::thread t2(func2);
t1.swap(t2); // t1 现在关联 func2,t2 关联 func1
t1.join();
t2.join();
其它的部分就不过多介绍,后面需要查表或询问ai即可。
总结表:
| 知识点 | 底层本质 |
|---|---|
| 构造立即启动 | 构造内部调用 pthread_create/CreateThread |
join() | 调用 pthread_join,调用线程进入内核态等待 |
detach() | 调用 pthread_detach,线程结束自动回收资源 |
| 移动语义 | 只复制/清空句柄,O(1),无系统调用 |
| 不可拷贝 | 拷贝句柄会导致资源管理混乱 |
| 必须 join/detach | 析构函数检查 _M_joinable,若为 true 则 terminate |
| 内存可见性 | 构造含 release,join 含 acquire,保证内存同步 |
核心法则:每个 joinable() == true 的 thread 对象,最终必须且只能调用一次 join() 或 detach()。
std::this_thread 命名空间
std::this_thread 是一个命名空间而非类,它提供的所有函数都是无状态的,作用是让当前线程认识自己、控制自己。

// libstdc++ 简化实现
namespace this_thread {
// 获取当前线程ID - 直接调用系统API
inline thread::id get_id() noexcept {
#ifdef __linux__
return thread::id(pthread_self()); // POSIX
#elif _WIN32
return thread::id(GetCurrentThreadId()); // Windows
#endif
}
// 让出CPU - 调用系统调度器
inline void yield() noexcept {
#ifdef __linux__
sched_yield(); // POSIX
#elif _WIN32
SwitchToThread(); // Windows
#endif
}
// 休眠 - 基于系统定时器
template<typename Rep, typename Period>
void sleep_for(const chrono::duration<Rep, Period>& rel_time) {
#ifdef __linux__
struct timespec ts = to_timespec(rel_time);
nanosleep(&ts, nullptr); // 或 clock_nanosleep
#elif _WIN32
Sleep(duration_cast<milliseconds>(rel_time).count());
#endif
}
// 休眠直到绝对时间点
template<typename Clock, typename Duration>
void sleep_until(const chrono::time_point<Clock, Duration>& abs_time) {
sleep_for(abs_time - Clock::now());
}
}
std::this_thread 的所有函数都是对操作系统系统调用的直接封装,不持有任何状态。
函数
| 方法 | 功能 | 用法示例 | 核心注意事项(必读) |
|---|---|---|---|
get_id() | 获取当前线程唯一标识 | auto id = std::this_thread::get_id(); | • 返回 std::thread::id,可作map key• 默认构造的 thread::id() 表示"无线程",不等于主线程ID• 线程结束后ID可能被系统回收重用 |
yield() | 主动让出CPU(仍可运行状态) | std::this_thread::yield(); | • 唯一用途:优化自旋锁,避免CPU 100% • 不保证其他线程一定会运行 • 不要用于延时/同步 |
sleep_for() | 休眠至少指定时长 | std::this_thread::sleep_for(100ms); | • 实际休眠 ≥ 指定时间(系统精度通常10-15ms) • 不用于精确计时 • sleep_for(0) 无效,请用 yield() |
sleep_until() | 休眠到绝对时间点 | std::this_thread::sleep_until(deadline); | • 周期性任务首选(自动补偿执行时间) • 时间点已过则立即返回 • 比 sleep_for 更适合定时循环 |
三个最容易犯的错误
| 错误写法 | 后果 | 正确写法 |
|---|---|---|
if (id == std::thread::id()) 判断主线程 | 永远为假 | 先保存 static auto main_id = std::this_thread::get_id() |
自旋锁:while(flag) {} | CPU 100% | while(flag) std::this_thread::yield(); |
sleep_for(1ms) 当作精确延时 | 实际可能15ms | 接受误差,或改用实时系统 |
二、mutex(锁)
锁的作用在之前的文章已经讲过,这里不复述。
std::mutex 核心方法
| 方法 | 功能 | 底层行为 | 注意事项 |
|---|---|---|---|
lock() | 加锁,若已被锁则阻塞等待 | 进入内核态,线程挂起 | 可能抛出 std::system_error |
unlock() | 解锁,唤醒等待线程 | 唤醒等待队列中的一个线程 | 只有锁的持有者才能调用 |
try_lock() | 尝试加锁,立即返回 | 不阻塞,测试并设置 | 返回 true 成功,false 已被锁 |
std::mutex mtx;
void safe_operation() {
mtx.lock();
// 临界区:访问共享数据
shared_data++;
mtx.unlock(); // 必须配对,否则死锁
}
错误:lock() 后忘记 unlock(),或中途抛出异常导致 unlock() 未执行 → 死锁
RAII 锁管理
std::lock_guard - 最简单的 RAII 锁
template<typename Mutex>
class lock_guard {
private:
Mutex& mtx;
public:
explicit lock_guard(Mutex& m) : mtx(m) { mtx.lock(); }
~lock_guard() { mtx.unlock(); }
// 禁止拷贝
lock_guard(const lock_guard&) = delete;
};
void safe_operation() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区:即使抛出异常,析构时也会自动解锁
shared_data++;
} // 自动解锁
std::unique_lock - 灵活的 RAII 锁
std::mutex mtx;
// 1. 延迟加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// ... 做一些不需要锁的操作
lock.lock(); // 手动加锁
// 2. 提前解锁
lock.unlock(); // 临界区已结束,提前释放
// 3. 尝试加锁
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock()) {
// 获得锁
} else {
// 未获得锁
}
// 4. 超时尝试(需要 timed_mutex)
std::unique_lock<std::timed_mutex> lock(mtx, std::chrono::milliseconds(100));
if (lock.owns_lock()) {
// 100ms 内获得了锁
}
// 5. 转移锁所有权
std::unique_lock<std::mutex> lock1(mtx);
std::unique_lock<std::mutex> lock2 = std::move(lock1); // lock1 不再持有锁
| 特性 | lock_guard | unique_lock |
|---|---|---|
| RAII 自动解锁 | ✅ | ✅ |
| 延迟加锁 | ❌ | ✅ |
| 提前解锁 | ❌ | ✅ |
| 超时尝试 | ❌ | ✅ |
| 移动语义 | ❌ | ✅ |
| 性能开销 | 极小 | 稍大(有状态 |
其他 mutex 类型
| 类型 | 特点 | 适用场景 |
|---|---|---|
std::mutex | 基础互斥锁,不支持递归 | 默认选择 |
std::recursive_mutex | 同一线程可多次加锁 | 递归函数、链式调用 |
std::timed_mutex | 支持超时 try_lock_for/until | 需要限时等待 |
std::recursive_timed_mutex | 递归 + 超时 | 复杂场景 |
std::shared_mutex(C++17) | 共享锁(读)/独占锁(写) | 读多写少 |
总结:
| 类型/函数 | 功能 | 关键点 |
|---|---|---|
mtx.lock() | 加锁,阻塞 | 必须配对 unlock(),否则死锁 |
mtx.unlock() | 解锁 | 只有持有者能调用 |
mtx.try_lock() | 尝试加锁,不阻塞 | 返回 bool |
lock_guard<M> | RAII 锁 | 最常用,构造加锁析构解锁 |
unique_lock<M> | 灵活 RAII 锁 | 支持延迟、提前、超时、移动 |
| `std::lock(m1, m2...) | 同时锁定多个锁 | 避免死锁的原子操作 | | ||
std::scoped_lock(C++17) | 多锁 RAII | 比 lock+guard 更简洁 |
std::adopt_lock | 标记锁已持有 | 配合 std::lock 使用 |
std::defer_lock | 标记延迟加锁 | unique_lock 专用 |
std::try_to_lock | 标记尝试加锁 | unique_lock 专用 |
mutex 保护共享数据,lock_guard 自动管理生命周期,std::lock 解决死锁问题。
call_once
std::call_once 是一个函数模板,配合 std::once_flag 使用,保证多个线程同时调用时,指定的可调用对象只被执行一次。
底层原理:
-
once_flag内部维护一个原子状态(未调用/调用中/已完成) -
第一个到达的线程执行函数,其他线程阻塞等待
-
执行完成后,所有等待线程被唤醒,后续调用直接跳过
// 简化实现示意 class once_flag { atomic<int> state; // 0=未调用, 1=调用中, 2=已完成 mutex mtx; condition_variable cv; }; template<typename Callable> void call_once(once_flag& flag, Callable&& func) { int expected = 0; if (flag.state.compare_exchange_strong(expected, 1)) { // 第一个线程:执行函数 func(); flag.state = 2; flag.cv.notify_all(); // 唤醒所有等待线程 } else if (flag.state == 1) { // 其他线程:等待完成 unique_lock lock(flag.mtx); flag.cv.wait(lock, [&]{ return flag.state == 2; }); } // state == 2:直接返回 }once_flag的职责分解组件 在 once_flag中作用 状态存储器 atomic<int>记住"执行过了吗" 竞争仲裁器 CAS 操作的目标 多个线程争抢"执行权" 等待队列 mutex+condition_variable(可选)让失败的线程阻塞等待 生命周期锚点 once_flag对象本身多个 call_once调用共用同一个状态once_flag本身几乎什么都不干,它是call_once的工作台和数据区。且once_flag必须是独立的,否则每个 call_once 调用都不知道别人是否调用过基本用法:
std::once_flag flag; void init_database(const std::string& host, int port) { std::cout << "连接数据库 " << host << ":" << port << std::endl; } void worker() { std::call_once(flag, init_database, "localhost", 3306); }异常处理
核心特性:如果函数抛出异常,
once_flag会重置状态,允许后续再次调用。std::once_flag flag; int attempt = 0; void unstable_init() { attempt++; std::cout << "第 " << attempt << " 次尝试" << std::endl; if (attempt < 3) { throw std::runtime_error("初始化失败"); } std::cout << "初始化成功" << std::endl; } void test() { try { std::call_once(flag, unstable_init); } catch (...) { // 异常后 flag 仍处于"未调用"状态 // 下次 call_once 会再次执行 } } int main() { test(); // 第1次:抛异常 test(); // 第2次:抛异常 test(); // 第3次:成功 test(); // 第4次:直接跳过(已成功) } // 输出: // 第 1 次尝试 // 第 2 次尝试 // 第 3 次尝试 // 初始化成功
-
更多推荐

所有评论(0)