第二章 线程管理
1.1 线程的启动:
Ø 通过函数对象构造thread对象,如下:
class background_task
{
public:
void operator()()const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
有件事需要注意,当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解析”(C++’s most vexing parse,)。如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。例如:
std::thread my_thread(background_task());
这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。使用在前面命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。
如下所示:
std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2
Ø 通过lambda表达式来构造线程对象,如下:
std::thread my_thread([]{
do_something();
do_something_else();
});
Ø 普通函数或者成员函数来构造线程对象,就不一一举例了。
1.2 线程的等待和分离:std::thread对象使用detach(),也是join()的使用条件,并且要用同样的方式进行检查——当std::thread对象使用t.joinable()返回的是true,就可以使用t.detach()。
1.3 线程函数传递参数:
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 2
display_status();
t.join();
process_widget_data(data); // 3
}
虽然update_data_for_widget①的第二个参数期待传入一个引用,但是std::thread的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。当线程调用update_data_for_widget函数时,传递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。因此,当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,且process_widget_data将会接收到没有修改的data变量③。可以使用std::ref将参数转换成引用的形式,从而可将线程的调用改为以下形式:
std::thread t(update_data_for_widget,w,std::ref(data));
另外常量引用使用std::cref()
1.4 虽然,std::thread实例不像std::unique_ptr那样能占有一个动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个执行线程。执行线程的所有权可以在多个std::thread实例中互相转移,这是依赖于std::thread实例的可移动且不可复制性。不可复制保性证了在同一时间点,一个std::thread实例只能关联一个执行线程;可移动性使得程序员可以自己决定,哪个实例拥有实际执行线程的所有权。注意:1)从临时对象std::thread获得转移权是不需要std::move()来显式移动所有权,从临时对象进行移动是自动和隐式的。2)如果实例已经关联了一个线程,再次赋值会导致std::terminate()被调用来终止程序。你不能仅仅通过向管理一个线程的对象赋值来“舍弃”原有线程
1.5 std::thread支持移动意味着所有权很容易从一个函数中被转移出,直接返回函数中的thread的局部对象或者临时对象,是以移动语意返回的
第三章 在线程间共享数据
2.1 互斥量mutex可以直接调用成员函数lock()和unlock()进行锁定和解锁,但是不推荐这么做,因为这意味着你必须记住在离开函数的每条代码路径上调用unlock(),包括异常分支,使用std::lock_guard类模板,实现了互斥量的RAII惯用语法,构造时锁定,析构时解锁。注意:如果一个成员函数返回对受保护的数据(加锁)的指针或引用,能够访问该指针或者引用的任意实现代码现在可以访问该数据而不需要获得该互斥量的锁,此外,还要检查有没有向其调用的,不在你掌控范围之内的函数传入共享数据的指针或引用。所以应该遵循一个原则:不要将受保护(共享、加锁)的数据的指针或引用传递到锁的范围之外,无论是通过函数返回、将其存放在外部可见的内存还是作为参数传递给用户提供的函数。
2.2 如何避免死锁?std::lock(lock1, lock2, ... ,lockn)可以同时锁住两个或多个互斥量且没有死锁的风险,如下:
// lock both mutexes without deadlock
std::lock(from.m, to.m);
// make sure both already-locked mutexes are unlocked at the end of scope
std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
// Equivalent code (if unique_locks are needed, e.g. for condition variables)
// std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
// std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
// std::lock(lk1, lk2);
std::adopt_lock是为了告知std::lock_guard对象该互斥量已经锁定,并且沿用互斥量上面的锁,而不是在构造函数中试图锁定互斥量。std::lock提供了关于锁定给定互斥量的全有或全无的语义。注意:只适用于同时获得两个或多个锁的情况下避免死锁,但是如果要分别获取锁,就没用了
2.3 避免死锁的一点建议:
· 避免嵌套锁
· 避免在持有锁时调用用户提供的代码
· 使用固定顺序获取锁
· 使用锁的层次结构
层次锁虽然不是标准的一部分,但是很容易实现,详见《c++并发编程实战》
2.4 std::unique_lock比std::lock_guard更加灵活,提供了lock(),try_lock()和unlock(三个成员函数,并且不总是拥有与之相关联的互斥量;
std::unique_lock可以在作用域之间转移锁的所有权,在函数返回一个实例的情况下,自动转移,其他情况下需要显示调用std::move()。从根本上来说,是左值和右值 的区别,右值来说是自动转移的,左值需要显式完成。
2.5 特殊情况下共享数据的保护:
初始化时的共享数据保护:常见的做法的是双重检查锁定(DoubleChecked Locking)
voidundefined_behaviour_with_double_checked_locking()
{
if(!resource_ptr)//(1)
{
std::lock_guard<std::mutex>lk(resource_mutex);
if(!resource_ptr)//(2)
{
resource_ptr.reset(new some_resource); //(3)
}
}
resource_ptr->do_something();//(4)
}
但是这种做法可能产生严重的数据竞争,因为锁外部的读取(1)与锁内部另一线程完成的写入不同步(3),导致(4)的调用不一定在正确的值上运行
利用call_once完成上述功能:
std::shared_ptr<some_resource>resource_ptr;
std::once_flag resource_flag;
void init_resource()
{
resource_ptr.reset(newsome_resource);
}
void foo()
{
std::call_once(resource_flag,init_resource);
resource_ptr->do_something();
}
std::call_once还可以用于线程安全的类成员延迟初始化:由于某些类可能在多个成员函数中触发类成员的初始化操作,但是该操作只需要进行一次,此时call_once可以派上用场
在c++11之前的编译器上,局部静态变量的初始化是线程不安全的,可能出现另一个线程访问还未初始化完成的静态变量,但是在c++11中该问题得到了解决:
cpp官网c++11中增加的关于静态局部变量的解释:
Ifmultiple threads attempt to initialize the same static local variableconcurrently, the initialization occurs exactly once (similar behavior can beobtained for arbitrary functions with std::call_once). (since C++11)
对于需要单一全局实例的场合,可以用局部静态变量替代std::call_once():
class my_class;
my_class& get_my_class_instance()
{
static my_class instance;
return instance;
}