C++并发编程实战笔记 [03] :在线程间共享数据
std::mutex
#include <mutex>
#include <list>
std::mutex some_mutex;
std::list<int> mylist;
void func(int value) {
some_mutex.lock(); // 加锁
mylist.push_back(value);
some_mutex.unlock(); // 解锁
}
std::lock_guard<>
类模板 std::lock_guard<>
使用了RAII技术,在构造时给互斥加锁,在析构时解锁。
#include <list>
#include <mutex>
std::list<int> mylist;
std::mutex some_mutex;
void foo(int value) {
std::lock_guard<std::mutex> guard(some_mutex); // 自动加锁
mylist.push_back(value);
} // 析构时解锁互斥
C++17新特性:类模板参数推导
std::lock_guard<std::mutex> guard(some_mutex); // before C++ 17
std::lock_guard guard(some_mutex); // C++ 17
std::lock()
需要锁住多个互斥时,为了防范死锁,应该始终按相同的顺序锁住互斥。std::lock()
帮我们解决了这一问题,它可以同时锁住多个互斥,而没有发生死锁的风险。
// 使用std::lock() 和 std::lock_guard<>,进行内部的数据互换操作
class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X {
private:
some_big_object some_detail;
std::mutex m;
public:
X(const some_big_object &sd) : some_detail(sd) {}
friend void swap(X& lhs, X& rhs) {
if(&lhs == &rhs) return;
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
}
std::adopt_lock
std::adopt_lock
指明互斥已被锁住,即互斥上有锁存在,std::lock_guard
实例应当据此接收互斥的归属权,不得在构造函数内试图另行加锁。
std::mutex m1, m2;
void func() {
std::lock(m1, m2);
std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock);
}
std::scoped_lock<>
std::scoped_lock<>
是C++17引入的增强版的lock_guard
,以多个互斥对象作为构造函数的参数列表,在构造时同时锁定多个互斥,在析构时同时解锁互斥。以上代码可以被简化为:
std::mutex m1, m2;
void func() {
std::scoped_lock guard(m1, m2);
}
建议
- 将互斥和受保护的数据组成一个类。
- 警惕成员函数返回指针或引用,若它们指向受保护的数据,互斥会被打破。
- 若成员函数在自身内部调用了别的函数,而这些函数却不受我们掌控,那么也不得向它们传递指针或引用。
- 警惕接口是否存在固有的条件竞争。
防范死锁
-
避免嵌套锁。假如已经持有锁,就不要试图获取第二个锁。如确有需要获取多个锁,应该使用
std::lock()
或std::scoped_lock<>
,一次性获取全部锁来避免死锁。 -
一旦持有锁,就必须避免调用由用户提供的程序接口。
-
依从固定顺序获取锁。如果多个锁是绝对必要的,却无法通过
std::lock()
在一步操作中全部获取,我们只能退而求其次,在每个线程内部都依从固定顺序获取这些锁。 -
按层级加锁。若某线程已对低层级互斥加锁,就不准它对高层级互斥加锁。
-
将准则推广到锁操作之外。死锁现象并不单单因加锁操作而发生,任何同步机制导致的循环都会导致死锁的出现。
std::unique_lock<>
std::unique_lock<>
更为灵活,不一定始终占有与之关联的互斥,但性能相比 std::lock_guard
较低。其构造函数接受第二个参数:可以传入 std::adopt_lock
实例,指明 std::unique_lock
对象管理互斥上的锁;也可以传入 std::defer_lock
实例,从而使互斥在完成构造时处于无锁状态,等以后有需要时才在 std::unique_lock
对象上调用 lock()
而获取锁,或把 std::unique_lock
对象交给 std::lock()
函数加锁。std::unique_lock
也允许它的实例在被销毁前解锁,其成员函数 unlock()
负责解锁操作。
std::mutex m1, m2;
void func() {
std::unique_lock<std::mutex> lock_a(m1, std::defer_lock);
std::unique_lock<std::mutex> lock_b(m2, std::defer_lock); // 实例 std::defer_lock 将互斥保留为无锁状态
std::lock(lock_a, lock_b); // 到这里才对互斥加锁
}
在不同作用域之间转移互斥归属权
std::unique_lock<std::mutex> get_lock() {
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk;
}
void process_data() {
std::unique_lock<std::mutex> lk(get_lock());
do_something();
}
std::call_once()
和 std::once_flag
令所有线程共同使用 std::call_once()
调用初始化函数,可以确保初始化由其中某线程安全且唯一地完成。必要的同步数据由 std::once_flag
实例存储,每个 std::once_flag
实例对应一次不同的初始化。std::once_flag
实例既不可复制也不可移动。
实现延迟初始化
std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;
void init_resource() {
resource_ptr.reset(new some_resource);
}
void foo() {
std::call_once(resource_flag, init_resource); // 初始化函数被准确地唯一一次调用
resource_ptr->do_something();
}
// 利用 std::call_once() 函数对类 X 的数据成员实施线程安全的延迟初始化
class X {
private:
connection_info connection_details;
connection_handle connection;
std::once_flag connection_init_flag;
void open_connection() {
connection = connection_manager.open(connection_details);
}
public:
X(const connection_info& connection_details_):
connection_details(connection_details_) {}
void send_data(const data_packet& data) {
std::call_once(connection_init_flag, &X::open_connection, this);
connection.send_data(data);
}
data_packet receive_data() {
std::call_once(connection_init_flag, &X::open_connection, this);
return connection.receive_data();
}
};
C++标准规定,只要控制流第一次遇到静态数据的声明语句,变量即进行初始化。C++11规定初始化只会在某一线程上单独发生。某些类的代码只需要用到唯一一个全局实例,这种情形可用以下方法代替 std::call_once
:
class my_class;
my_class& get_my_class_instance() {
static my_class instance;
return instance;
}
读写互斥
允许单独一个 “写线程” 进行完全排他的访问,也允许多个 “读线程” 共享数据或并发访问。
排他锁(写锁)
std::lock_guard<std::shared_mutex>
std::unique_lock<std::shared_mutex>
共享锁(读锁)
std::shared_lock<std::shared_mutex>
多个线程能够同时锁住同一个 std::shared_mutex
。若共享锁已被某些线程所持有,若别的线程试图获取排他锁,就会发生阻塞,直到那些线程全部都释放该共享锁。反之,若任一线程持有排他锁,那么其他线程全部无法获取共享锁或排他锁,直到持锁线程将排他锁释放为止。
// 运用 std::shared_mutex 保护数据结构,以简易的 DNS 缓存表为例
#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>
class dns_entry;
class dns_cache {
private:
std::map<std::string, dns_entry> entries;
mutable std::shared_mutex entry_mutex;
public:
dns_entry find_entry(const std::string& domain) const { // 多线程可以同时调用
std::shared_lock<std::shared_mutex> lk(entry_mutex); // 共享锁
const std::map<std::string, dns_entry>::const_iterator it = entries.find(domain);
return (it == entries.end() ? dns_entry() : it->second);
}
void update_or_add_entry(const std::string& domain, const dns_entry& dns_details) { // 多线程进行排他访问
std::lock_guard<std::shared_mutex> lk(entry_mutex); // 排他锁
entries[domain] = dns_details;
}
};