文章

转移所有权

线程所有权通过移动语义转移,确保唯一管理,避免资源冲突和程序崩溃。

转移所有权

转移所有权

在 C++ 中,std::thread 是一个资源占有类型,它的行为类似于 std::unique_ptr可移动,但不可复制。这样设计的目的,是为了确保线程的所有权始终只有一个明确的拥有者,避免多线程并发错误。

为什么要支持移动?

当我们创建线程并希望将线程的所有权传递给其他对象(比如函数返回值、容器等)时,必须通过移动操作来实现。

示例:线程所有权的转移

1
2
3
4
5
6
7
8
9
10
11
void some_function();
void some_other_function();

std::thread t1(some_function);         // 1 创建并启动线程
std::thread t2 = std::move(t1);        // 2 转移所有权到 t2
t1 = std::thread(some_other_function); // 3 启动另一个线程,隐式移动到 t1

std::thread t3;                        // 4 默认构造,无线程
t3 = std::move(t2);                    // 5 t2 的线程所有权转移到 t3

t1 = std::move(t3);                    // 6 ❌ 错误:t1 已拥有线程,导致 std::terminate()
  • t1 在① 时拥有线程,② 后 t1 不再拥有线程,t2 拥有。
  • ③ 中创建的是临时 std::thread,可直接移动给 t1
  • ⑤ 中使用 std::move(t2) 明确地将 t2 的线程所有权转移给 t3
  • ⑥ 尝试将 t3 的线程所有权赋值给已拥有线程的 t1,违反规则,程序会触发 std::terminate()

示例:函数返回 std::thread

1
2
3
4
5
6
7
8
9
10
std::thread f() {
    void some_function();
    return std::thread(some_function); // 返回临时线程对象,发生隐式移动
}

std::thread g() {
    void some_other_function(int);
    std::thread t(some_other_function, 42);
    return t; // 返回局部变量,触发移动构造
}

示例:将线程传入函数

1
2
3
4
5
6
7
8
9
void f(std::thread t); // 参数通过移动方式获取线程所有权

void g() {
    void some_function();
    f(std::thread(some_function)); // 传入临时对象,隐式移动
    
    std::thread t(some_function);
    f(std::move(t));               // 显式移动已有线程对象
}

scoped_thread —— 防止线程泄露

定义

scoped_thread 是一个自定义类,它不是 C++ 标准库自带的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class scoped_thread {
    std::thread t; // 成员变量,拥有一个 std::thread 实例,负责管理一个线程

public:
    // 显式构造函数:通过移动构造的方式接收一个线程对象
    explicit scoped_thread(std::thread t_)
        : t(std::move(t_)) // 移动线程所有权到成员变量 t
    {
        // 如果传入的线程不可 join(说明它不是一个有效线程),抛出逻辑错误异常
        if (!t.joinable())
            throw std::logic_error("No thread");
    }

    // 析构函数:当 scoped_thread 对象销毁时,自动 join 所管理的线程
    ~scoped_thread() {
        t.join(); // 等待线程执行结束,防止主线程结束后线程未完成
    }

    // 禁用拷贝构造函数,防止复制 scoped_thread(因为线程只能唯一拥有)
    scoped_thread(const scoped_thread&) = delete;

    // 禁用拷贝赋值操作,防止复制赋值
    scoped_thread& operator=(const scoped_thread&) = delete;
};

用法示例

1
2
3
4
5
6
7
struct func; // 假设定义在其他地方

void f() {
    int some_local_state;
    scoped_thread t(std::thread(func(some_local_state))); // 线程交给 scoped_thread 管理
    do_something_in_current_thread();
} // 离开作用域自动 join

相比手动 join,使用 RAII 的方式可以自动管理线程生命周期,避免资源泄露。

C++20:建议加入 std::jthread

由于 scoped_thread 的思想太有用,C++20 引入了 std::jthread 类型,功能类似于:

joining_thread 示例实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 一个封装 std::thread 的类,支持 RAII 管理线程,析构时自动 join
class joining_thread {
    std::thread t; // 内部持有的 std::thread 对象,代表一个可执行线程

public:
    // 默认构造函数,线程未初始化
    joining_thread() noexcept = default;

    // 构造函数:接受任意可调用对象及其参数,完美转发给 std::thread 构造线程
    template<typename Callable, typename... Args>
    explicit joining_thread(Callable&& func, Args&&... args)
        : t(std::forward<Callable>(func), std::forward<Args>(args)...) {}

    // 构造函数:从已有的 std::thread 对象中移动构造
    explicit joining_thread(std::thread t_) noexcept : t(std::move(t_)) {}

    // 移动构造函数:支持 joining_thread 之间移动所有权
    joining_thread(joining_thread&& other) noexcept : t(std::move(other.t)) {}

    // 移动赋值操作:如果当前对象持有线程,则先 join,避免线程泄漏
    joining_thread& operator=(joining_thread&& other) noexcept {
        if (joinable()) {      // 当前线程有效,先等它执行完
            join();
        }
        t = std::move(other.t); // 接管另一个线程的所有权
        return *this;
    }

    // 接受 std::thread 作为右值赋值:先 join 当前线程,然后接管新线程
    joining_thread& operator=(std::thread other) noexcept {
        if (joinable()) {
            join();
        }
        t = std::move(other);
        return *this;
    }

    // 析构函数:如果线程有效且尚未 join,则自动 join
    ~joining_thread() noexcept {
        if (joinable())
            join();
    }

    // 交换两个 joining_thread 对象所持有的线程(成员 swap)
    void swap(joining_thread& other) noexcept {
        t.swap(other.t);
    }

    // 返回当前线程的 ID
    std::thread::id get_id() const noexcept {
        return t.get_id();
    }

    // 判断线程是否可以 join(即线程是否仍然活跃)
    bool joinable() const noexcept {
        return t.joinable();
    }

    // 手动调用 join,等待线程结束
    void join() {
        t.join();
    }

    // 手动调用 detach,使线程在后台执行并与当前对象解绑
    void detach() {
        t.detach();
    }

    // 获取对内部 std::thread 的引用(用于底层访问)
    std::thread& as_thread() noexcept {
        return t;
    }

    // 获取对内部 std::thread 的 const 引用(用于只读访问)
    const std::thread& as_thread() const noexcept {
        return t;
    }
};

设计要点:

特性说明
RAII 自动管理线程析构时自动 join,避免忘记调用造成崩溃
支持移动,不支持复制保证线程所有权唯一(默认 std::thread 不可复制)
与原生 std::thread 互操作提供 as_thread() 可访问底层线程对象
安全移动赋值赋值前自动 join 当前线程,避免资源泄漏或未定义行为

示例:std::thread vs std::jthread

使用 std::thread(C++11 起)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <thread>
#include <chrono>

void worker_thread() {
    std::cout << "std::thread 工作线程开始\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "std::thread 工作线程结束\n";
}

void std_thread_demo() {
    std::thread t(worker_thread);

    // 如果忘记 join,会崩溃!
    if (t.joinable()) {
        t.join();
    }
}
使用 std::jthread(C++20 起)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token> // C++20 中用于取消支持
#include <functional>

void worker_jthread(std::stop_token st) {
    std::cout << "std::jthread 工作线程开始\n";
    for (int i = 0; i < 5; ++i) {
        if (st.stop_requested()) {
            std::cout << "std::jthread 收到取消请求,提前退出\n";
            return;
        }
        std::cout << "std::jthread 执行中...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "std::jthread 工作线程结束\n";
}

void jthread_demo() {
    std::jthread t(worker_jthread);

    // 主线程等一会后请求取消
    std::this_thread::sleep_for(std::chrono::milliseconds(1200));
    t.request_stop();  // 向线程发送取消信号

    // 不需要手动 join,自动完成
}
  • 隐式注入 stop_token 参数机制:标准库内部会自动为函数添加一个 std::stop_token 参数并传进去,只要函数的第一个参数类型正好是 std::stop_token

输出示例

1
2
3
4
5
6
7
std::thread 工作线程开始
std::thread 工作线程结束

std::jthread 工作线程开始
std::jthread 执行中...
std::jthread 执行中...
std::jthread 收到取消请求提前退出

在容器中管理线程

可以把多个 std::thread 存入 std::vector,实现统一创建和管理。

示例:创建多个线程并等待它们结束

1
2
3
4
5
6
7
8
9
10
11
12
13
void do_work(unsigned id);

void f() {
    std::vector<std::thread> threads;

    for (unsigned i = 0; i < 20; ++i) {
        threads.emplace_back(do_work, i); // 通过 emplace_back 直接构造线程
    }

    for (auto& t : threads) {
        t.join(); // 统一等待所有线程结束
    }
}

适用场景:任务划分明确、互不依赖的线程,适合批量并发执行。

总结

内容说明
std::thread 可移动不可复制保证线程所有权唯一性
std::move(thread)显式转移所有权,防止悬空或重复管理
scoped_threadRAII 风格线程管理,确保 join
joining_threadC++20 建议替代方案,更通用
容器管理线程使用 std::vector<std::thread> 统一管理
本文由作者按照 CC BY 4.0 进行授权