《C++并发编程实战》读书笔记(2):线程间共享数据

打印 上一主题 下一主题

主题 910|帖子 910|积分 2730

1、使用互斥量

在C++中,我们通过构造std::mutex的实例来创建互斥量,调用成员函数lock()对其加锁,调用unlock()解锁。但通常更推荐的做法是使用标准库提供的类模板std::lock_guard,它针对互斥量实现了RAII手法:在构造时给互斥量加锁,析构时解锁。两个类都在头文件里声明。
  1. std::list<int> some_list;
  2. std::mutex some_mutex;
  3. void add_to_list(int value)
  4. {
  5.     //C++17引入了类模板参数推导的新特性,所以下面语句也可以简化成:std::lock_guard guard(some_mutex);
  6.     std::lock_guard<std::mutex> guard(some_mutex);
  7.     some_list.push_back(value);
  8. }
  9. bool list_contains(int value)
  10. {
  11.     std::lock_guard<std::mutex> guard(some_mutex);
  12.     return std::find(some_list.begin(), some_list.end(), value) != some_list.end();
  13. }
复制代码
2、防范死锁

假设有两个线程,都需要同时锁住两个互斥量才能进行某种操作,但它们分别只锁住了一个互斥量,都等着再给另一个互斥量加锁,这就构成了死锁。标准库提供了std::lock函数来解决死锁的问题,它可以同时锁住多个互斥量。
  1. class some_big_object {};
  2. void swap(some_big_object& lhs, some_big_object& rhs) {}
  3. class X
  4. {
  5. private:
  6.     some_big_object some_detail;
  7.     mutable std::mutex m;
  8. public:
  9.     X(const some_big_object& sd) :some_detail(sd) {}
  10.     friend void swap(X& lhs, X& rhs)
  11.     {
  12.         if (&lhs == &rhs) { return; }
  13.         std::lock(lhs.m, rhs.m);
  14.         std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
  15.         std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
  16.         swap(lhs.some_detail, rhs.some_detail);
  17.     }
  18. };
复制代码
本例中必须要判断两个参数是否指向不同的实例,因为如果已经在某个std::mutex对象上加锁,那么再次试图加锁将导致未定义的行为。构造std::lock_guard对象时,额外参数std::adopt_lock指明互斥量已被锁住,std::lock_guard实例应当据此接收锁的归属权,不得在构造函数内试图另行加锁。
针对上述场景,C++17还提供了新的RAII模板类std::scoped_lock,它和std::lock_guard完全等价,只不过前者是可变参数模板,接收各种互斥量型别作为模板参数列表,还以多个互斥量对象作为构造函数参数列表。下列代码中,传入构造函数的两个互斥量都被加锁,机制与std::lock()函数相同,因此,当构造函数完成时它们都被锁定,而后在析构函数内一起被解锁。
  1. void swap(X& lhs, X& rhs)
  2. {
  3.     if (&lhs == &rhs) { return; }
  4.     //这里使用了C++17的类模板参数推导特性,下面的语句完全等价于std::scoped_lock<std::mutex, std::mutex> guard(lhs.m, rhs.m);
  5.     std::scoped_lock guard(lhs.m, rhs.m);
  6.     swap(lhs.some_detail, rhs.some_detail);
  7. }
复制代码
标准库也提供了std::unique_lock模板,它与std::lock_guard一样,也是一个以互斥量作为参数的类模板,并且以RAII手法管理锁,不过它更灵活一些(代价是略微损失性能)。std::unique_lock的构造函数接收第二个参数,我们可以传入std::adopt_lock以指明std::unique_lock对象管理互斥量上的锁,也可以传入std::defer_lock使互斥量在完成构造时处于无锁状态,等以后有需要时再加锁。
  1. void swap(X& lhs, X& rhs)
  2. {
  3.     if (&lhs == &rhs) { return; }
  4.     std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
  5.     std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
  6.     std::lock(lock_a, lock_b);
  7.     swap(lhs.some_detail, rhs.some_detail);
  8. }
复制代码
std::unique_lock类十分灵活,它具有成员函数lock()、try_lock()、unlock(),这与互斥量的基本成员函数一致,所以该类可以结合泛型函数来使用,例如std::lock()。std::unique_lock的实例可以在销毁前通过成员函数unlock()解锁,这意味着如果执行流程的任何特定分支没有必要继续持有锁,那我们就可以提前解锁,这在有些情况下可能有助于提升程序性能。
锁的归属权可以在多个std::unique_lock实例之间转移,比如一个函数锁定互斥量,然后把锁的归属权转移给函数的调用者,好让它在同一个锁的保护下执行其它操作,例如:
  1. std::unique_lock<std::mutex> get_lock()
  2. {
  3.     extern std::mutex some_mutex;
  4.     std::unique_lock<std::mutex> lk(some_mutex);
  5.     prepare_data();
  6.     return lk;
  7. }
  8. void process_data()
  9. {
  10.     std::unique_lock<std::mutex> lk(get_lock());
  11.     do_something();
  12. }
复制代码
3、保护共享数据的其它工具

3.1、保护共享数据的初始化

假设共享数据只在初始化过程中需要保护,此后无需再进行显式的同步操作,那么可以使用std::once_flag类和std::call_once函数来处理这种情况,它们可以保证初始化操作只会执行一次。std::once_flag的实例既不可复制也不可移动,这与std::mutex类似。
  1. std::shared_ptr<some_resource> resource_ptr;
  2. std::once_flag resource_flag;
  3. void init_resource()
  4. {
  5.     resource_ptr.reset(new some_resource);
  6. }
  7. void foo()
  8. {
  9.     std::call_once(resource_flag, init_resource);
  10.     resource_ptr->do_something();
  11. }
复制代码
C++11规定了局部静态变量的初始化只会在某个单一线程上发生,在初始化完成之前,其它线程不会越过静态数据的声明而继续运行。如果某些类只需要用到唯一一个全局实例,这种情况下可以用以下方法代替std::call_once:
  1. class my_class;
  2. my_class& get_my_class_instance()
  3. {
  4.     static my_class instance;
  5.     return instance;
  6. }
复制代码
3.2、保护不常更新的数据

如果我们想要允许单独一个“写线程”进行完全排他的访问,也允许多个“读线程”共享数据或并发访问,那么可以使用C++17提供的新互斥量std::shared_mutex。对于更新操作,使用std::lock_guard或std::unique_lock锁定,代替对应的std::mutex特化,它们都保证了访问的排他性质。对于无需更新数据结构的线程,可以另行改用共享锁std::shared_lock,多个线程能够同时锁住同一个std::shared_mutex。
  1. class dns_entry {};
  2. class dns_cache
  3. {
  4.     std::map<std::string, dns_entry> entries;
  5.     std::shared_mutex entry_mutex;
  6. public:
  7.     dns_entry find_entry(const std::string& domain)
  8.     {
  9.         std::shared_lock<std::shared_mutex> lk(entry_mutex);
  10.         auto it = entries.find(domain);
  11.         return it == entries.end() ? dns_entry() : it->second;
  12.     }
  13.     void update_or_add_entry(const std::string& domain, const dns_entry& dns_details)
  14.     {
  15.         std::lock_guard<std::shared_mutex> lk(entry_mutex);
  16.         entries[domain] = dns_details;
  17.     }
  18. };
复制代码
3.3、递归加锁

标准库提供了std::recursive_mutex,其工作方式与std::mutex相似,不同之处是其允许同一线程对某互斥量的同一实例多次加锁。假如我们对它调用3次lock(),就必须调用3次unlock()才能解锁。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

杀鸡焉用牛刀

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表