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::mapstd::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 知识体系总览

下面这张图把线程管理的核心操作串起来了,方便你整体把握:

线程管理 移动语义 不可拷贝,只能 std::move 容器操作需显式移动 线程标识 t.get_id() 外部获取 this_thread::get_id() 内部获取 休眠与yield sleep_for / sleep_until yield 让出CPU时间片 核心原则 移动语义保证所有权唯一 · 标识用于调试和日志 · 休眠避免忙等

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