3. 线程管理:移动语义、标识获取、休眠与yield
线程管理这部分,说实话是并发编程里最基础也最容易踩坑的地方。我早年做后台服务时,就因为线程生命周期没管好,线上出了好几次事故。今天咱们就把这块彻底捋清楚。
3.1 线程的移动语义
先聊聊移动语义。C++11 的 std::thread 是不可复制的,但可以移动。为什么?你想想看,一个线程一旦启动,它就代表着一个执行上下文。如果允许复制,那语义就乱了——到底哪个对象拥有这个线程?
核心规则:std::thread 只能移动,不能拷贝。移动后,源线程对象不再关联任何执行线程。
#include <thread>
#include <iostream>
void worker() {
std::cout << "线程工作中\n";
}
int main() {
std::thread t1(worker);
// std::thread t2 = t1; // 编译错误!不能拷贝
std::thread t2 = std::move(t1); // 正确:移动语义
// 此时 t1 不再关联任何线程
if (!t1.joinable()) {
std::cout << "t1 已空,安全\n";
}
t2.join();
return 0;
}
我个人习惯在函数返回线程时,直接返回局部 std::thread 对象。编译器会优先使用移动语义,效率很高。
std::thread createWorker() {
return std::thread(worker); // 移动构造,高效
}
避坑指南:我曾经在项目里把线程对象塞进 std::vector,结果忘记用 std::move,编译报错半天没反应过来。记住:容器操作线程对象时,必须显式移动。
std::vector<std::thread> threads;
threads.push_back(std::thread(worker)); // 临时对象,自动移动
threads.emplace_back(worker); // 更简洁,直接构造
std::thread t(worker);
threads.push_back(std::move(t)); // 已存在的对象,必须 move
3.2 线程的标识与获取
每个线程都有一个唯一的标识符,类型是 std::thread::id。这东西在调试和日志里特别有用。我当年排查死锁问题时,就是靠线程 ID 定位到具体是哪个线程卡住了。
获取线程 ID 有两种方式:
- 外部获取:调用
t.get_id(),返回线程t的 ID - 内部获取:在线程函数里调用
std::this_thread::get_id()
#include <thread>
#include <iostream>
#include <vector>
void logWork(int taskId) {
auto tid = std::this_thread::get_id();
std::cout << "任务 " << taskId
<< " 运行在线程 " << tid << "\n";
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back(logWork, i);
}
for (auto& t : workers) {
std::cout << "管理线程 ID: " << t.get_id() << "\n";
t.join();
}
return 0;
}
std::thread::id 支持比较和哈希,所以你可以把它放进 std::map 或 std::unordered_map 里做线程专属存储。嗯,这里要注意:空线程(未关联任何执行线程)的 ID 是默认构造的,所有空线程的 ID 都相等。
注意:线程 ID 在操作系统层面可能被复用。但 C++ 标准保证:只要线程还在运行,其 std::thread::id 就是唯一的。线程结束后,ID 可能被新线程重用。
3.3 线程的休眠与 yield
这两个操作看着简单,但用不好会出大问题。我见过有人用 sleep 做同步,结果性能惨不忍睹。
3.3.1 std::this_thread::sleep_for 和 sleep_until
sleep_for 让当前线程暂停一段指定的时间。sleep_until 则是睡到某个绝对时间点。说白了,一个相对时间,一个绝对时间。
#include <thread>
#include <chrono>
#include <iostream>
void periodicTask() {
for (int i = 0; i < 5; ++i) {
std::cout << "第 " << i << " 次心跳\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main() {
auto start = std::chrono::steady_clock::now();
auto deadline = start + std::chrono::seconds(3);
std::thread t([deadline]() {
std::this_thread::sleep_until(deadline);
std::cout << "睡到指定时间才醒\n";
});
t.join();
return 0;
}
个人经验:我在做定时任务调度时,更倾向用 sleep_until。因为 sleep_for 受任务执行时间影响,累积误差会越来越大。用 sleep_until 指定绝对时间点,精度更可控。
3.3.2 std::this_thread::yield
yield 是个有意思的函数。它告诉调度器:「我现在没什么事做,把 CPU 让给其他线程吧。」但注意,这只是个建议,调度器不一定采纳。
#include <thread>
#include <iostream>
#include <atomic>
std::atomic<bool> ready(false);
void spinWait() {
while (!ready.load()) {
std::this_thread::yield(); // 让出 CPU,避免忙等
}
std::cout << "条件满足,继续执行\n";
}
int main() {
std::thread t(spinWait);
std::this_thread::sleep_for(std::chrono::seconds(1));
ready.store(true);
t.join();
return 0;
}
你想想看,如果没有 yield,上面的自旋锁会疯狂占用 CPU。加了 yield 后,线程在等待期间会主动让出时间片,系统负载明显下降。
我曾经踩过的坑:在低负载场景下滥用 yield。如果系统里只有一两个活跃线程,yield 反而会导致上下文切换开销增加,性能不升反降。说白了,yield 适合高竞争场景,不适合轻负载。
3.4 知识体系总览
下面这张图把线程管理的核心操作串起来了,方便你整体把握:
3.5 综合示例:线程池的简单实现
最后,我把今天讲的知识点揉进一个简单的线程池里。你感受一下实际项目中怎么用:
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <iostream>
class SimpleThreadPool {
public:
SimpleThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task(); // 执行任务
}
});
}
}
void enqueue(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.push(std::move(task));
}
cv.notify_one();
}
~SimpleThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) {
if (t.joinable()) {
t.join(); // 等待所有线程结束
}
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
};
int main() {
SimpleThreadPool pool(4);
for (int i = 0; i < 8; ++i) {
pool.enqueue([i]() {
auto tid = std::this_thread::get_id();
std::cout << "任务 " << i
<< " 由线程 " << tid << " 执行\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}
// 析构时自动等待所有任务完成
return 0;
}
这个例子用到了线程的移动语义(workers.emplace_back)、线程标识(get_id())、以及休眠(sleep_for)。你仔细看看,每个知识点都用上了。
总结一下:线程管理说白了就是三件事——谁拥有线程(移动语义)、谁在执行(标识获取)、什么时候让出CPU(休眠与yield)。把这三点吃透,并发编程的基础就算打牢了。
公众号:蓝海资料掘金营,微信deep3321