20230610thread

Jun 10, 2023

thread

基于进程开发,有助于隔离物理、逻辑、上下文的好处。基于线程开发,有助于有效利用多核心CPU优势,充分发挥性能上的优势,比如常用来比较、衡量单机处理并发的多少。

thread.h 是一组跨平台的多线程库,并且提供简单的使用即可实现多线程技术。

创建一个 std::thread对象即可启动一个线程,并使用该std::thread对象来管理该线程。.e.g. std::thread t(afunc, param1, param2...)

本文所学来自于Ref

一个简单的多线程实现

在4个线程中分别输出索引:

1
2
3
4
5
6
7
8
9
10
11
void output(int i) {
cout << i << endl;
}
int main() {
for (uint8_t i = 0; i < 4; i++) {
thread t(output, i);
t.detach();
}
getchar(); // 为了防止主线程提前结束,而子线程还未运行完的情况。
return 0;
}

t 为线程分配的对象,output 为线程启动的方法块,启动后该方法获得输入索引i

单线程能简单预测得到输出:

1
2
3
4
0
1
2
3

但是在单线程中多次运行可能得到以下结果:

1
2
3
4
01

2
3

这就涉及到多线程编程最核心的问题: 资源竞争.

假设CPU有4核,可以同时执行4个线程,但是控制台却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出. 将上面代码创建的四个线程进行编号: t0,t1,t2,t3, 分别输出的数字: 0,1,2,3. 参照上图的执行结果,控制台的拥有权的转移如下:

  • t0拥有控制权,输出了数字0,但是其没来的及输出换行符,控制的拥有权却转移到了t1; (0)
  • t1完成自己的输出,t1线程完成; (1\n)
  • 控制台拥有权转移给t0,输出换行符; (\n)
  • t2拥有控制台,完成输出; (2\n)
  • t3拥有控制台,完成输出; (3\n)

由于控制台是系统资源,这里控制台拥有权的管理是操作系统完成的。但是,假如是多个线程共享进程空间的数据,这就需要自己写代码控制,每个线程合适能够拥有共享数据进行操作。

启动一个线程

  • 可以使用lambda表达式:
1
2
3
4
5
6
7
8
for (int i = 0; i < 4; i++) {
thread t([i]{
cout << i << endl;
});
t.detach();
// t.join();
}
getchar();

t.detach(); 使分线程进入后台执行;t.join(); 使分线程加入到当前线程,当分线程结束后回到当前线程,继续往下走。

  • 重载了()运算符的类的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Task {
public:
void operator()(int i) {
cout << i << endl;
}
};
int main() {
for (uint8_t i = 0; i < 4; i++) {
Task task;
thread t(task, i);
t.detach();
}
getchar();
}

异常情况下等待线程完成

当决定以detach方式让线程在后台运行时,可以在创建thread 的实例后立即调用detach, 这样线程就会和thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证线程在后台运行。

但线程以join方式运行时,需要在主线程的合适位置调用join 方法,如果调用join前出现了异常,thread被销毁,线程就会被异常所终结,或者由于某些原因,例如线程访问了局部变量,就要保证线程一定要在函数退出前完成,就要保证要在函数退出前调用join.

一种比较好的方法是自愿获取即初始化(RALL, Resource Acquisition Is Initialization), 该方法提供一个类,在析构函数中调用join:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class thread_guard {
thread *t;
public:
explicit thread_guard(thread& _t): t(_t) {}
~thread_guard() {
if (t.joinable())
t.join();
}
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
int main() {
thread t([] {
cout << "Hello thread" << endl;
});
thread_guard g(t);
}

向线程传递参数

线程启动的函数参数,可以为指针或者对象拷贝。如果使用引用拷贝的话,其引用的是拷贝的线程空间的对象,而不是出事希望改变的对象。指针传参如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TagNode {
public:
int a;
int b;
};
void func(TagNode* node) {
node->a = 10;
node->b = 20;
}
int main() {
TagNode node;
thread t(func, &node);
t.join();

cout << node.a << endl; // 10
cout << node.b << endl; // 20
return 0;
}

转移线程的所有权

thread 是可移动的,但不可复制。可以通过move 来改变线程的所有权,灵活的决定线程在什么时候join或者detach.

1
2
thread t1(f1);
thread t3(move(t1));

讲线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.joint1.detach 会出现异常。

Ref

thread