「C++ 多线程」std::thread scoped_thread 实现

文章大图来源:pixiv_id=104139863

scoped_thread 定义

scoped_thread 是一种资源管理类,用于确保线程在适当的时候被正确地加入(join)或分离(detach),更好地管理线程资源。scoped_thread 利用了 RAII(Resource Acquisition Is Initialization)机制,也可以称为 资源获取即初始化

scoped_thread 实现及作用

std::thread 成员

通常我们在 scoped_thread 类中定义一个私有 std::thread 对象成员变量,用于存储实际的线程对象。scoped_thread 类可以对这个 std::thread 线程进行管理,控制其生命周期等操作。

构造函数的编写

  • explicit 关键字

    使用 explicit 关键字修饰构造函数,是为了防止隐式转换。这意味着该构造函数只能通过显式地传入 std::thread 对象来进行调用,而不可以通过一些其他类型隐式地转换为 std::thread 对象传入构造函数。

  • std::move 移动语义

    当 scoped_thread 对象被创建时,获取一个 std::thread 线程对象 t_ ,并且通过 std::move 移动语义将这个 std::thread 对象的线程 t_ 归属权转移到 scoped_thread 对象的成员变量 t 中。

    采用移动语义,也更加符合线程的资源管理方式,符合线程具有独占性所有权的特点,高效实现线程归属权的转移,能够避免不必要的拷贝操作。

  • 线程是否有效

    在构造函数体中,通过 if(!t.joinable()){...} 语句来检查接收到的线程对象是否关联了函数且有效(joinable)。

    有可能传入的 std::thread 对象是一个空线程(没有关联任何线程接口函数),或者已经 join()detach() 过了。如果出现这些情况,需要抛出异常。

析构函数的编写

在析构函数中,首先通过 if(t.joinable()){...} 再次检查 std::thread 成员变量 t 所代表的线程对象是否是有效可连接的。如果已经 join()detach() 过了,就不需要再进行后续的操作了。

如果线程 t 确定是 joinable 的,那么调用 join() 函数。这使得 scoped_thread 对象的线程 t 在主线程 main 执行结束时调用析构函数,进而自动调用 join(),让主线程等待 scoped_thread 对象的线程 t(也即子线程)直到其执行完成。

通过在析构函数中执行 join() 操作,利用了 RAII 机制,保证了无论在什么情况下,线程都能被正确地结束,避免了线程被意外中断等结果。

禁用拷贝与拷贝赋值

众所周知,std::thread 是不可以拷贝和拷贝赋值的,因此我们定义的 scoped_thread 类也需要禁用拷贝构造函数和拷贝赋值函数(声明为 = delete),避免出现多个 scoped_thread 对象同时管理一个线程的混乱情况(一个 std::thread 同一时刻只能拥有一个线程的归属权)。

scoped_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
#include <iostream>
#include <thread>

class scoped_thread{
std::thread t;
public:
// 构造函数,采用移动语义(防止隐式转换)
explicit scoped_thread(std::thread t_): t(std::move(t_)){
if(!t.joinable()){ // 检查线程是否有效
throw std::logic_error("thread is unjoinable.");
}
}
// 析构函数
~scoped_thread(){
if(t.joinable()){
t.join(); // 析构时自动 join
}
}
// 禁用拷贝构造函数
scoped_thread(scoped_thread const&) = delete;
// 禁用拷贝赋值函数
scoped_thread& operator=(scoped_thread const&) = delete;
};

// 线程接口函数
void foo(int cnt){
for(int i = 1; i <= cnt; i ++ ){
std::cout << "Kirisame Marisa." << i << "\n";
}
}

int main(){
scoped_thread st(std::thread(foo, 10));

// 主线程中模拟做些其他事情
for(int i = 1; i <= 3; i ++ ){
std::cout << "Alice Margatroid. " << i << "\n";
}

return 0;
}

运行结果如下:

可以看出,scoped_thread 对象的子线程正常执行完毕。

当主线程结束时,我们定义的 scoped_thread 对象 st 也超出了作用域,此时会自动调用析构函数。在我们实现的 scoped_thread 类的析构函数中,会自动执行 join() 操作,等待关联的线程(关联函数 foo())执行完成,确保线程资源被正确处理。之后 main 函数才会真正结束,整个程序正常退出。

参考

  1. C++ 并发编程实战 第 2 章 线程管理

「C++ 多线程」std::thread scoped_thread 实现
https://marisamagic.github.io/2024/12/22/20241222/
作者
MarisaMagic
发布于
2024年12月22日
许可协议