最近这段时间在学习C++多线程相关的知识,打算将学习的内容记录下来,加深理解和记忆。
C++11 新标准中引入了五个头文件来支持多线程编程,他们分别是<atomic>
,<thread>
,<mutex>
,<condition_variable>
和<future>
。
<atomic>
:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套C风格的原子类型和与C兼容的原子操作的函数。<thread>
:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。<mutex>
:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。<condition_variable>
:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。<future>
:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。
定义
C++是在C++11之后才有了线程库:std::thread。编译时需要添加选项:-std=c++11。
使用std::thread创建线程比较简单,thread实例化一个线程对象就创建完成了,示例:
#include <iostream>
#include <thread>
void f() {
std::cout << "hello world";
}
int main() {
std::thread t{f};
t.join(); // 等待新起的线程退出
}
main() 函数的是主线程,将函数f()添加为std::thread的参数即可启动另一个线程,两个线程会同时运行。
构造函数
默认构造函数 | thread() noexcept; |
---|---|
初始化构造函数 | template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args); |
拷贝构造函数 [deleted] | thread(const thread&) = delete; |
Move构造函数 | thread(thread&& x) noexcept; |
- 默认构造函数,创建一个空的
std::thread
执行对象。 - 初始化构造函数,创建一个
std::thread
对象,该std::thread
对象可被joinable
,新产生的线程会调用fn
函数,该函数的参数由args
给出。 - 拷贝构造函数(被禁用),意味着
std::thread
对象不可拷贝构造。 - Move构造函数,move构造函数(move语义是C++11新出现的概念),调用成功之后
x
不代表任何std::thread
执行对象。
析构函数
~thread();
销毁thread对象。如果它还拥有关联线程(joinable() == true
),则会调用std::terminate()结束程序。一般需要下列操作后,thread对象无关联的线程才可以安全销毁:
- 被默认构造
- 被移动(转移所有权)
- 已调用join()
- 已调用detach()
赋值操作函数
如果该对象还拥有关联的运行中进程(即joinable() == true
),则调用std::terminate()
终止程序。否则,赋值other
的状态给该对象并设置other
为默认构造的状态(空状态,不再执行线程)。
thread& operator=( thread&& other ) noexcept;
注意:该操作与move构造函数一样,属于“剪切”而非“拷贝”。
join与datch
join:阻塞当前线程直至thread对象所标识的线程结束其执行。
void join();
detach:从thread对象分离执行线程,允许独立地执行线程,主调线程无法再取得该线程的控制权。detach调用后不需要再调用join等待线程结束释放资源。一旦该线程退出,则自动释放所有分配的资源(它的资源会被init
进程回收)。
void detach();
其它
joinable:检查对象是否还标识活跃的执行线程。具体就是,若get_id() != std::thread::id()
则返回true,否则false 。默认构造的thread因为没有执行线程所以返回false。 结束执行代码,但仍未调用join函数的线程仍被当作活跃的执行线程,从而返回true。
bool joinable() const noexcept;
get_id:返回标识与当前thread对象关联的线程的std::thread::id
。也就是返回线程的唯一标识。类std::thread::id
是轻量的可频繁复制类,它是std::thread
对象的唯一标识符。此类的实例也保留有不表示任何线程的特殊值。一旦线程结束,则std::thread::id
的值可为另一线程复用。此类也可以用作有序和无序的关联容器的键值。
std::thread::id get_id() const noexcept;
native_handle:返回实现定义的底层线程句柄。允许通过使用平台相关API直接操作底层实现。具体值要依赖具体平台,对于Linux而言,即返回pthread的句柄。
native_handle_type native_handle();
hardware_concurrency:返回实现支持的并发线程数。应该只把该值当做提示。当无法获取时,函数返回0。
static unsigned int hardware_concurrency() noexcept;
基本用法
上面已经给出实例,传递一个函数指针就可以实例化一个线程,而std::thread的参数也可以使用有函数操作符类型的对象实例或者Lambda表达式进行构造:
#include <iostream>
#include <thread>
struct A {
// 函数操作符重载
void operator()() const {
std::cout << 1;
}
};
int main() {
A a;
std::thread t1(a); // 1 会调用 A 的操作符()函数
std::thread t2(A()); // 2 most vexing parse,声明名为t2参数类型为A的函数
std::thread t3{A()}; // 3
std::thread t4((A())); // 4
std::thread t5{[] { std::cout << 1; }}; // 5
t1.join();
t3.join();
t4.join();
t5.join();
}
对于上面语句①,通过有函数操作符类型的实例进行构造,也就是类型A的实例a,thread会将对象a复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。
语句②,传递了一个临时变量A(),而不是一个命名的变量。C++编译器会将其解析为函数声明,而不是类型对象的定义。即t2是一个函数,原型是std::thread t2(A (*)())
,这个函数带有一个参数(函数指针指向没有参数并返回A对象的函数),返回一个std::thread对象的函数。
为了避免出现类似语句②那样的语法失误,可以使用多组括号③,或使用统一的初始化语法④都可以避免。
语句⑤,Lambda表达式也能避免这个问题。Lambda表达式是C++11的一个新特性,允许使用一个可以捕获局部变量的局部函数。
线程参数
向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加参数即可。需要注意的是,这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。来看一个例子:
#include <thread>
#include <string>
#include <iostream>
void f(int i, std::string const& s) {
std::cout << s << i << std::endl;
}
int main() {
std::thread t(f, 3, "hello");
t.join();
}
上述代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型,线程的上下文完成字面值向std::string的转化。
如果函数参数定义了默认实参则会被忽略,也就是必须要指定一个函数实参,即使有默认参数。
#include <iostream>
#include <thread>
void f(int i = 1) {}
int main() {
// std::thread t{f}; // 出错,因为默认实参会被忽略
std::thread t{f, 42};
t.join();
}
如果参数是引用类型也会被忽略,如下面的代码,std::thread的构造函数①并不知晓函数f需要传入引用参数,直接无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用f()。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。
#include <iostream>
#include <thread>
void f(int& x) { ++x; }
int main() {
int i = 1;
std::thread t{f, i}; // 1 compile error
t.join();
std::cout << i << std::endl;
}
问题的解决办法很简单:如果参数是引用类型要使用std::ref,使用std::ref将参数转换成引用的形式,这样函数f()就会收到i的引用,而非i的拷贝副本,例子如下:
#include <iostream>
#include <thread>
void f(int& x) { ++x; }
int main() {
int i = 1;
std::thread t{f, std::ref(i)};
t.join();
std::cout << i << std::endl; // 输出 2
}
thread构建也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:
#include <iostream>
#include <thread>
struct A {
public:
void do_work() {
std::cout << "A::do_work\n";
}
};
int main() {
A a;
std::thread t(&A::do_work, &a); // 1
t.join();
}
上面这段代码中,新线程将会调用a.do_work(),其中a的地址作为对象指针提供给函数。
这种情况也可以为成员函数提供参数:std::thread构造函数的第三个参数就是成员函数的第一个参数,以此类推:
#include <iostream>
#include <thread>
struct A {
public:
void do_work(int num) {
std::cout << "A::do_work\n";
}
};
int main() {
A a;
int i = 1;
std::thread t(&A::do_work, &a, i); // 1
t.join();
}
另一种情况,为线程提供的入口函数的参数仅支持移动(move),不能拷贝。“移动”是指原始对象中的数据所有权转移给另一对象,从而这些数据就不再在原始对象中保存(类似文本编辑时的剪切操作)。std::unique_ptr就是这样一种类型(C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制。同一时间内,只允许一个std::unique_ptr实例指向一个对象,并且当这个实例销毁时,指向的对象也将被删除。移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权在多个std::unique_ptr实例中传递。使用“std::move”转移对象所有权后,就会留下一个空指针。使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()进行显示移动。下面的代码展示了std::move的用法,展示了std::move是如何转移动态对象的所有权到线程中去的:
#include <iostream>
#include <thread>
struct big_object {
public:
void prepare_data(int num) {
std::cout << "big_object::prepare_data\n";
}
};
void process_big_object(std::unique_ptr<big_object> up) {
std::cout << "process_big_object\n";
}
int main() {
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p)); // 1
t.join();
}
通过在std::thread构造函数中执行std::move§,big_object对象的所有权首先被转移到新创建线程的的内部存储中,之后再传递给process_big_object函数。
等待线程完成(Join)
在线程销毁前要对其调用join等待线程退出或detach将线程分离,以下程序属于使用join正常等待线程退出,join属于阻塞式接口:
#include <iostream>
#include <thread>
#include <chrono>
std::time_t now()
{
auto t0 = std::chrono::system_clock::now();
std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
return time_t_today; // seconds
}
void foo()
{
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << now() << "-ending first helper...\n";
}
void bar()
{
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << now() << "-ending second helper...\n";
}
int main()
{
std::cout << "starting first helper...\n";
std::thread helper1(foo);
std::cout << "starting second helper...\n";
std::thread helper2(bar);
std::cout << now() << "-waiting for helpers to finish..." << std::endl;
helper1.join();
std::cout << now() << "-join return first helper...\n";
helper2.join();
std::cout << now() << "-join return second helper...\n";
std::cout << "finish!\n";
}
输出
starting first helper...
starting second helper...
1645140155-waiting for helpers to finish...
1645140156-ending second helper...
1645140160-ending first helper...
1645140160-join return first helper...
1645140160-join return second helper...
finish!
使用detach分离线程,注意分离线程可能出现空悬引用的隐患:
#include <iostream>
#include <thread>
#include <chrono>
class A {
public:
A(int& x) : x_(x) {}
void operator()() {
std::cout << "before sleep" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "after sleep" << std::endl;
call(x_); // 存在对象析构后引用空悬的隐患
}
private:
void call(int& x) {
std::cout << x << std::endl;
}
private:
int& x_;
};
void f() {
int x = 0;
A a{x}; // 1 x的引用传递给A.0
std::cout << "before t" << std::endl;
std::thread t{a}; // 2
std::this_thread::sleep_for(std::chrono::seconds(1));
t.detach(); // 3 不等待 t 结束
std::cout << "after t.detach" << std::endl;
} // 4 函数结束后 t 可能还在运行,而 x 已经销毁,a.x_ 为空悬引用
int main() {
std::thread t{f}; // 5 导致空悬引用
t.join();
std::cout << "finish" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); // 6
}
输出:
before t
before sleep
after t.detach
finish
after sleep
0
如果std::thread线程结束后没有调用join释放资源,thread的析构函数会调用std::terminate终止程序,如下程序就会运行出错:
#include <thread>
int main() {
{
std::thread t([] {});
// t.join();
}
}
// terminate called without an active exception
join会在线程结束后清理std::thread所有资源,使其与完成的线程不再关联,因此对一个线程只能进行一次 join,如果调用多次会抛出异常:
#include <thread>
int main() {
std::thread t([] {});
t.join();
t.join(); // 错误throw excaption
}
// 抛出的异常错误如下:
// minate called after throwing an instance of 'std::system_error'
// What(): Invalid argument
如果线程运行过程中发生异常(通常抛出异常要么会终止程序,要么跳转到捕获异常的位置),之后的join会被忽略,为此需要捕获异常,并在抛出异常前join:
#include <iostream>
#include <thread>
int main() {
std::thread t([] {});
try {
std::cout << "throw 0" << std::endl;
throw 1; // 1
} catch (int x) {
std::cout << "catch: " << x << std::endl;
t.join(); // 2 处理异常前先 join()
throw x; // 3 再将异常抛出
}
std::cout << "last join" << std::endl;
t.join(); // 4 之前抛异常,不会执行到此处
}
输出:
terminate called after throwing an instance of 'int'
throw 0
catch: 1
从上面的输出来看,根本不会执行到最后语句④,因为上面抛出异常程序就直接结束了,这个例子就是要说明thread创建的线程需要使用join()确保在线程完成后清理线程相关的资源,否则会引起程序异常。
特殊情况下的等待
对于上面发生异常导致程序终止的情况,很容易就忘记调用join,为了避免应用被抛出的异常所终止,通常我们使用另外一种方式解决该问题,封装一个类,在析构函数中使用join()。如同下面代码:
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):
t(t_)
{}
~thread_guard()
{
if(t.joinable()) // 1
{
t.join(); // 2
}
}
thread_guard(thread_guard const&)=delete; // 3
thread_guard& operator=(thread_guard const&)=delete;
};
void th_func() {
std::cout << "th_func" << std::endl;
}
void f()
{
std::thread t(th_func);
thread_guard g(t);
do_something_in_current_thread();
} // 4
int main() {
f();
}
线程执行到④处时,局部对象就要被逆序销毁了。因此,thread_guard对象g是第一个被销毁的,这时线程在析构函数中被加入②到原始线程中。即使do_something_in_current_thread抛出一个异常,这个销毁依旧会发生。
在thread_guard析构函数的测试中,首先判断线程是否可汇入①。如果可汇入,会调用join()②进行汇入。
拷贝构造函数和拷贝赋值操作标记为=delete
③,是为了不让编译器自动生成。直接对对象进行拷贝或赋值是很危险的,因为这可能会弄丢已汇入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。
如果不想等待线程结束,可以分离线程,从而避免异常。不过,分离操作即使能让线程仍然在后台运行着,也能确保在std::thread
对象销毁时不调用std::terminate()
。但这会打破了线程与std::thread
对象的联系,也就是外部无法通过thread实例对象操作控制线程了。
转移所有权
std::thread是move-only类型,不能拷贝,只能通过移动转移所有权(复制构造函数已被删除),但不能转移所有权到joinable的线程,因为每个线程thread实例都是唯一的,没有两个std::thread对象会表示同一执行线程。
#include <thread>
#include <cassert>
#include <utility>
void f() {}
void g() {}
int main() {
std::thread t1{f}; // 1
std::thread t2 = std::move(t1); // 2
assert(!t1.joinable());
assert(t2.joinable());
t1 = std::thread{g}; // 3
assert(t1.joinable());
assert(t2.joinable());
// t1 = std::move(t2); // 4 错误,不能转移所有权到 joinable 的线程
t1.join();
t1 = std::move(t2); // 5
assert(t1.joinable());
assert(!t2.joinable());
t1.join();
}
首先,新线程与t1相关联①。当显式使用std::move()
创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行f的函数线程与t2关联。
然后,临时std::thread
对象相关的线程启动了③。为什么不显式调用std::move()
转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用。
将t2线程的所有权转移④给t1。不过,t1已经有了一个关联的线程(执行g的线程),所以这里系统直接调用std::terminate()
终止程序继续运行。这样做(不抛出异常,std::terminate()
是noexcept函数)是为了保证与std::thread
的析构函数的行为一致。需要在线程对象析构前,显式的等待线程完成,或者分离它,进行赋值时也需要满足这些条件(说明:不能通过赋新值给std::thread
对象的方式来"丢弃"一个线程)。
可以看到调用join释放线程资源后就可以使用move转移所有权了⑤。
- 移动操作同样适用于支持移动的容器
#include <algorithm>
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> v;
for (int i = 0; i < 10; ++i) {
v.emplace_back([] {});
}
std::for_each(std::begin(v), std::end(v), std::mem_fn(&std::thread::join));
}
将std::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,而是把它们当做一个组。创建一组线程(数量可以在运行时确定)。
- std::thread 可以作为函数返回值
#include <thread>
std::thread f() {
return std::thread{[] {}};
}
int main() {
std::thread t{f()}; // f函数返回的thread移交给了t
t.join();
}
- std::thread 也可以作为函数参数
#include <thread>
#include <utility>
void f(std::thread t) { t.join(); }
int main() {
f(std::thread([] {}));
std::thread t([] {});
f(std::move(t));
}
实现一个可以直接用std::thread构造的自动清理线程的类
#include <stdexcept>
#include <thread>
#include <utility>
class scoped_thread {
public:
explicit scoped_thread(std::thread x) : // 1
t_(std::move(x)) {
if (!t_.joinable()) { // 2
throw std::logic_error("no thread");
}
}
~scoped_thread() {
t_.join(); // 3
}
scoped_thread(const scoped_thread&) = delete;
scoped_thread& operator=(const scoped_thread&) = delete;
private:
std::thread t_;
};
void f() {
scoped_thread t{std::thread{[] {}}}; // 4
} // 5
int main() {
f();
}
与上面实现的thread_guard相似,不过新线程会直接传递到scoped_thread中④,而非创建一个独立变量。当主线程到达f()末尾时⑤,scoped_thread对象就会销毁,然后在析构函数中完成汇入③。上面的thread_guard类,需要在析构中检查线程是否“可汇入”。这里把检查放在了构造函数中②,并且当线程不可汇入时抛出异常。
线程标识
线程标识为std::thread::id
类型,可以通过两种方式进行检索。
- 可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值(一般是0),这个值表示“无线程”。
- 在当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可以获得线程标识。
std::thread::id对象可以自由的拷贝和对比,因为标识符是唯一的。如果两个对象的std::thread::id相等,那就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。
C++线程库不会限制你去检查线程标识是否一样,std::thread::id
类型对象提供了相当丰富的对比操作。比如,为不同的值进行排序。这意味着开发者可以将其当做为容器的键值做排序,或做其他比较。按默认顺序比较不同的std::thread::id
:当a<b
,b<c
时,得a<c
,等等,标准库也提供std::hash<std::thread::id>
容器,std::thread::id
也可以作为无序容器的键值。
#include <iostream>
#include <thread>
#include <vector>
void th_func() {
std::cout << "sub_thread id2: " << std::this_thread::get_id() << std::endl;
}
void f()
{
std::thread t_null;
std::cout << "null_thread id: " << t_null.get_id() << std::endl;
std::thread t(th_func);
std::cout << "sub_thread id1: " << t.get_id() << std::endl;
t.join();
} // 4
int main() {
std::thread::id main_thread = std::this_thread::get_id();
std::cout << "main_thread id: " << main_thread << std::endl;
f();
std::vector<std::thread> threads(3); // 5
for(int i=0; i < 3; ++i)
{
threads[i]=std::thread([i] () {
std::cout << i << std::endl;
});
}
for (auto& it : threads) {
it.join();
}
}
输出:
main_thread id: 140672360355648
null_thread id: thread::id of a non-executing thread
sub_thread id1: 140672360351488
sub_thread id2: 140672360351488
0
2
1
查看硬件支持的线程数量
std::thread::hardware_concurrency()
在新版C++中非常有用,其会返回并发线程的数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个标识,当无法获取时,函数返回0。
#include <iostream>
#include <thread>
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << n << " concurrent threads are supported.\n";
}
std::thread到这里基本完成了,接口不是很多,但是用法细节挺多的。学习时最好都能用一个简单例子跑一遍。
参考:
《C++ Concurrency In Action》