C++多线程编程(一):使用thread类

C++多线程编程(一):使用thread类

千呼万唤始出来!在C++11这一核弹级更新中加入了对多线程编程的支持,从此程序猿们就可以在不借助Boost等第三方库(或者自己造轮子)情况下方便地编写跨平台的多线程程序。我将在《C++多线程编程》这一系列博客中介绍C++11中多线程编程的相关知识。让我们先从thread类开始。

thread类的功能

thread类是C++中表示执行线程的类,位于头文件<thread>中。我们创建一个thread对象就会立即执行一个对应的线程。通过thread类的成员函数,我们可以标识线程或对线程进行控制。

创建线程

启动线程

我们构造一个thread类的对象,就会立即执行一个与该thread对象对应的线程。通常我们会向thread类的构造函数传递一个可调用对象,新创建的线程就会执行这个可调用对象。可调用对象可以是函数函数指针lambda表达式function对象

栗子:

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

using namespace std;

void func(void)
{
cout << "hello world" << endl;
}

int main(void)
{
void (*func_ptr)(void) = func;
function<void(void)> func_obj = func;

thread t1(func);
thread t2(func_ptr);
thread t3(func_obj);
thread t4([]
{ cout << "hello world" << endl; });

t1.join();
t2.join();
t3.join();
t4.join();

return 0;
}

在上面的例子中,我们分别向thread类的构造函数传递了函数函数指针function对象lambda表达式。创建出的四个线程都是向标准输出中输出**”hello world”**。那么thread对象的join成员函数是干什么呢?我将在“连接线程”这一节中向大家介绍。

传递参数

既然thread类的构造函数可以接受一个可调用对象,那么可不可以想这个可调用参数传递参数呢?当然可以!如果我们想要想可调用对象传递参数,只需要将参数和可调用对象一并放入thread类的构造函数的参数列表中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>
#include <thread>

using namespace std;

void add(int n1, int n2)
{
cout << n1 << " + " << n2 << " = " << n1 + n2 << endl;
}

int main(void)
{
thread t1(add, 2, 3);
thread t2([](int n1, int n2)
{ cout << n1 << " + " << n2 << " = " << n1 + n2 << endl; },
5, 6);

t1.join();
t2.join();

return 0;
}

在以上的例子中,我们向thread类传入可调用对象和两个参数,可调用对象在执行时输出两个参数的和。

thread类的构造函数可以接受任意多的额外的参数,只需要传入的参数与可调用对象的参数列表一一对应即可。

连接线程与分离线程

连接线程

在上一节的两个例子中,我们均调用了thread对象的join成员函数。join成员函数的作用是等待线程完成。请读者试想一下,假如我们在main函数中创建了一个线程对象A,线程A在被创建之后执行了一个非常耗时的任务。而main函数(主线程)在创建完线程A后执行return 0,整个进程就会被终止,此时线程A即使没有执行完成也会被终止,程序的只想结果往往就不是我们所期望的了。所以我们需要在主线程中调用join成员函数来等线程执行完成,这个操作也叫连接线程。当我们调用join时,如果线程尚未执行完成,就对阻塞调用join的线程直至线程执行完成。若干在调用join时线程已经执行完成,则join会立即返回,不会阻塞调用线程。

分离线程

如果我们不需要等待线程完成,可以调用thread对象的detach成员函数分离线程,之后这个线程就可以独立运行,不需要我们调用join等待它执行完成。分离线程通常用于执行一些后台任务的线程。

可连接线程与不可连接线程

如果一个thread对象是符合以下任意一种情况,它将是不可连接的:

  1. 默认构造的thread对象。
  2. 该thread对象已经被移动到另一个thread对象。
  3. thread对象已经被连接或分离。

不能对不可连接的线程对象调用join。

线程与thread类析构函数

当一个thread对象被析构时,如果该thread对象对应的线程还没有执行完成,线程仍然会继续执行,不会因为thread对象被析构而停止执行。

如果一个线程是可连接的,则必须在thread对象被析构前调用join,否则析构函数会调用std::terminate终止程序。

线程标识

thread::id类

在C++中用thread::id类来标识线程。对于可连接线程,可以通过get_id成员函数获得标识该thread对象的thread::id对象,每个thread对象的id唯一。对于不可连接线程,调用get_id会返回默认构造的thread_id对象,所有不可连接线程的id相等。

thread::id类重载了一下运算符用于thread::id对象的相等性比较:

1
2
3
4
5
6
bool operator== (thread::id lhs, thread::id rhs) noexcept;
bool operator!= (thread::id lhs, thread::id rhs) noexcept;
bool operator< (thread::id lhs, thread::id rhs) noexcept;
bool operator<= (thread::id lhs, thread::id rhs) noexcept;
bool operator> (thread::id lhs, thread::id rhs) noexcept;
bool operator>= (thread::id lhs, thread::id rhs) noexcept;

thread::id还重载了<<运算符用于向输出流中输出thread::id:

1
2
template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);

获取当前线程的thread::id

通过调用命名空间this_thread中的静态函数get_id可以获得当前线程的线程id。

获取原生线程句柄

通过调用thread对象的native_handle成员函数可以获得平台相关的原生线程句柄,原生线程句柄类型thread::native_handle_type与实现有关,可能会影响程序的可移植性,不建议在一般情况下使用。

转移线程对象的所有权

thread类的拷贝构造函数被定义为删除的,所以thread对象只可以被移动,不能被拷贝。

this_thread的命名空间

this_thread命名空间提供了访问当前线程的一些函数,除了上文提到的get_id外,还有yieldsleep_untilsleep_for

yield

调用yield函数可以让出当前线程,让操作系统调度同一进程的其他线程。

sleep_until

sleep_until可以阻塞调用线程直至某个时间点。函数原型为:

1
2
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);

sleep_for

sleep_for可以在制定的时间跨度内阻塞线程的执行。函数原型为:

1
2
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);

检测硬件并发数

通过调用thread类的hardware_concurrency静态函数可以获得硬件线程的上下文数量,也就是硬件可以真正同时执行的线程的数目,返回值通常为逻辑CPU的数目

参考文献

  1. 《C++并发编程实战》
  2. thread - C++ Reference

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!