C++多线程编程(一):使用thread类
千呼万唤始出来!在C++11这一核弹级更新中加入了对多线程编程的支持,从此程序猿们就可以在不借助Boost等第三方库(或者自己造轮子)情况下方便地编写跨平台的多线程程序。我将在《C++多线程编程》
这一系列博客中介绍C++11中多线程编程的相关知识。让我们先从thread类开始。
thread类的功能
thread类
是C++中表示执行线程
的类,位于头文件<thread>
中。我们创建一个thread对象就会立即执行一个对应的线程。通过thread类的成员函数,我们可以标识线程或对线程进行控制。
创建线程
启动线程
我们构造一个thread类的对象,就会立即执行一个与该thread对象对应的线程。通常我们会向thread类的构造函数传递一个可调用对象
,新创建的线程就会执行这个可调用对象。可调用对象可以是函数
、函数指针
、lambda表达式
或function对象
。
栗子:
#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类的构造函数的参数列表中即可:
#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对象是符合以下任意一种情况,它将是不可连接的:
- 默认构造的thread对象。
- 该thread对象已经被移动到另一个thread对象。
- 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对象的相等性比较:
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:
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
外,还有yield
、sleep_until
和sleep_for
。
yield
调用yield函数可以让出当前线程,让操作系统调度同一进程的其他线程。
sleep_until
sleep_until
可以阻塞调用线程直至某个时间点。函数原型为:
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
sleep_for
sleep_for
可以在制定的时间跨度内阻塞线程的执行。函数原型为:
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
检测硬件并发数
通过调用thread类的hardware_concurrency静态函数
可以获得硬件线程的上下文数量,也就是硬件可以真正同时执行的线程的数目,返回值通常为逻辑CPU的数目
。
参考文献
- 《C++并发编程实战》
- thread - C++ Reference