Skykey's Home

Skykey的私人博客ᕕ( ᐛ )ᕗ

关于std::thread的二三事

关于std::thread的二三事

写在前面

很早之前写过一篇关于线程池文章,《基于C++11实现线程池》,一直被网友翻出来,对里面的一些细节实现提了许多问题。其实更推荐大家去阅读后来写的另外一篇关于线程池的文章,《当我谈线程池时我谈些什么——线程池学习笔记》。原因很简单,写第一篇线程池文章的时候,自己属实是菜的不行(现在依旧是菜的不行),花了很长时间去学习一些里面用到的C++11的新语法,文章里面讲的重点也是涉及到的C++11语法。所以,更关心线程池本身的实现的话,更适合去看新文章《当我谈线程池时我谈些什么——线程池学习笔记》,作为现代C++语法的新学者,想花更多精力在学习现代C++语法的话,适合去看老文章《基于C++11实现线程池》。

言归正传,在那篇老文章里有挺多网友提了一些std::thread相关的问题。今天看了会cppreference,写了点代码实践了下,集中回答了一些问题,顺便水篇文章算是学习笔记了。

std::thread对象构建的新线程何时开始执行

线程在构造关联的线程对象时立即开始执行,从提供给作为构造函数参数的顶层函数开始。

有几点需要注意:

  • 顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用std::terminate。在需要获取返回值时,顶层函数可以通过std::promise或者修改共享变量(可能需要锁机制进行线程同步)。
  • 当使用不带参数的默认构造函数thread()构造std::thread对象时,该对象不表示任何线程,也不会有新线程产生。
  • 当时用移动构造函数thread(thread&&other)构造std::thread对象时,该对象会被构造为表示曾为other所表示的执行线程的std::thread对象。此调用后other不再表示执行线程。
  • std::thread对象不可复制(复制构造函数已被删除)。没有两个std::thread对象会表示同一执行线程。

std::thread对象构建新线程时可以传入什么东西作为参数

std::thread常用的构造函数如下:

1
2
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

其中,f为任意可调用对象(Callable),args为任意数目的作为可调用对象f的参数。

可调用对象(Callable)是C++的一个具名要求,常见的函数、成员函数、仿函数(函数对象)都属于可调用对象。

函数

传入函数的情况最常见也最简单,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void f1(int n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 1 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main()
{
int n = 0;
std::thread t(f1, n + 1);
t.join();
return 0;
}

仅需将函数名与函数参数分别传入即可。

成员函数

当需要传入类时,有两种情况,第一种是比较复杂的情况,我们需要传入类的成员函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class foo
{
public:
void bar()
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 3 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int n = 0;
};

int main()
{
int n = 0;
foo f;
std::thread t(&foo::bar, &f); // t 在对象 f 上运行 foo::bar()
t.join();
return 0;
}

我们需要以std::thread(&类名::成员函数名, &类实例)的格式传入新线程。

仿函数(函数对象)

仿函数(或称函数对象)便是传入类的第二种情况,此时该类的工作比较简单(单一,并非指实际工作难度),例如标准库中的std::function, std::bind等,又例如第一篇文章中的ThreadWorker类。成为仿函数的类,一般来说需要**重载函数调用运算符()**。在std::thread对象构建新线程后,会自动进行INVOKE操作执行传入的可调用对象。INVOKE操作执行对象为仿函数时,会自动调用仿函数重载的函数调用运算符operator()。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class baz
{
public:
void operator()()
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 4 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int n = 0;
};

int main()
{
std::thread t(b); // t 在对象 b 的副本上运行 baz::operator()
t.join();
return 0;
}

需要注意的是,新线程运行的仿函数实际上是传入时指定的仿函数的副本,这同时也就要求该仿函数是可拷贝的。

函数参数传引用

std::thread对象构造新线程时,会移动或按值复制线程函数的参数。若需要传递引用参数给线程函数,则必须包装它(例如用std::refstd::cref)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void f2(int& n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 2 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main()
{
int n = 0;
std::thread t(f2, std::ref(n)); // 按引用传递
t.join();
return 0;
}

暂时就总结了这些,后续若有内容再更新。

更多

std::thread-cppreference.com

C++并发编程

C++内存模型