1. Использование мьютексов в C++
Стандартная библиотека C++ предоставляет класс шаблона синтаксиса RAII std::lack_guard для мьютексов, который будет предоставлять заблокированные мьютексы во время создания и разблокировать их во время разрушения, тем самым гарантируя заблокированный мьютекс Мьютекс всегда правильно разблокирован
Защитите список с помощью мьютекса
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // 3
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // 4
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}
Тщательно организовать код для защиты общих данных.
Непреднамеренно передавать ссылки на защищенные данные.
class some_data
{
int a;
std::string b;
public:
void do_something();
};
class data_wrapper
{
private:
some_data data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func)
{
std::lock_guard<std::mutex> l(m);
func(data); // 1 传递“保护”数据给用户函数
}
};
some_data* unprotected;
void malicious_function(some_data& protected_data)
{
unprotected=&protected_data;
}
data_wrapper x;
void foo()
{
x.process_data(malicious_function); // 2 传递一个恶意函数
unprotected->do_something(); // 3 在无保护的情况下访问保护数据
}
Проблема с этим кодом в том, что он вообще не защищает: он просто помечает все доступные коды структур данных как взаимоисключающие. Код, вызывающий unprotected->do_something() в функции foo(), не был помечен как взаимоисключающий.
В этом случае библиотека потоков C++ не может помочь, и программист должен использовать правильный мьютекс для защиты данных.
С оптимистической точки зрения, есть еще путь: никогда не передавать указатели или ссылки на защищенные данные вне области действия мьютекса, будь то возвращаемое значение функции, хранящееся во внешней видимой памяти или переданное в качестве аргумента в пользовательская функция.
2. Варианты разрешения условной конкуренции
Вариант 1: передать ссылку
Вариант 2: конструктор копирования или перемещения, который не создает исключений
Вариант 3: вернуть указатель на извлеченное значение
Вот пример
#include <exception>
#include <memory>
#include <mutex>
#include <stack>
struct empty_stack: std::exception
{
const char* what() const throw() {
};
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m;
public:
threadsafe_stack(){
}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
data = other.data; // 1 在构造函数体中的执行拷贝
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value)
{
std::lock_guard<std::mutex> lock(m);
data.push(new_value);
}
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack(); // 在调用pop前,检查栈是否为空
std::shared_ptr<T> const res(std::make_shared<T>(data.top())); // 在修改堆栈前,分配出返回值
data.pop();
return res;
}
void pop(T& value)
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value=data.top();
data.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
3. Тупик
std::lock - может блокировать несколько (более двух) мьютексов одновременно и не имеет побочных эффектов (риск взаимоблокировки).
// 这里的std::lock()需要包含<mutex>头文件
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(some_big_object const& sd):some_detail(sd){
}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::lock(lhs.m,rhs.m); // 1 std::lock不会自动解锁,需要给每个锁加上std::lock_guard
std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // 2
//提供std::adopt_lock参数除了表示std::lock_guard对象已经上锁外,还表示现成的锁,而非尝试创建新的锁
std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); // 3
swap(lhs.some_detail,rhs.some_detail);
}
};
Предотвращение взаимоблокировок
1. Избегайте вложенных блокировок, блокируйте несколько мьютексов с помощью std::lock
2. Избегайте вызова пользовательского кода при удержании блокировки
3. Используйте фиксированный порядок получения блокировок
1. Используйте иерархические блокировки, чтобы избежать
hierarchical_mutex high_level_mutex(10000); // 1
hierarchical_mutex low_level_mutex(5000); // 2
int do_low_level_stuff();
int low_level_func()
{
std::lock_guard<hierarchical_mutex> lk(low_level_mutex); // 3
return do_low_level_stuff();
}
void high_level_stuff(int some_param);
void high_level_func()
{
std::lock_guard<hierarchical_mutex> lk(high_level_mutex); // 4
high_level_stuff(low_level_func()); // 5
}
void thread_a() // 6
{
high_level_func();
}
hierarchical_mutex other_mutex(100); // 7
void do_other_stuff();
void other_stuff()
{
high_level_func(); // 8
do_other_stuff();
}
void thread_b() // 9
{
std::lock_guard<hierarchical_mutex> lk(other_mutex); // 10
other_stuff();
}
thread_b() не будет работать гладко, во-первых, он блокирует other_mutex⑩, а значение уровня этого мьютекса всего 100⑦. Это означает, что данные сверхнизкого уровня (данные сверхнизкого уровня) были защищены. Когда other_stuff() вызывает high_level_func()⑧, иерархия нарушается.
2. Используйте реализацию мьютекса уровня hierarchical_mutex
class hierarchical_mutex
{
std::mutex internal_mutex;
unsigned long const hierarchy_value;
unsigned long previous_hierarchy_value;
static thread_local unsigned long this_thread_hierarchy_value; // 1 被初始化为最大值
void check_for_hierarchy_violation()
{
if(this_thread_hierarchy_value <= hierarchy_value) // 2 第二个互斥量的层级值必须小于已经持有互斥量检查函数才能通过
{
throw std::logic_error(“mutex hierarchy violated”);
}
}
void update_hierarchy_value()
{
previous_hierarchy_value=this_thread_hierarchy_value; // 3
this_thread_hierarchy_value=hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value):
hierarchy_value(value),
previous_hierarchy_value(0)
{
}
void lock()
{
check_for_hierarchy_violation();
internal_mutex.lock(); // 4
update_hierarchy_value(); // 5
}
void unlock()
{
this_thread_hierarchy_value=previous_hierarchy_value; // 6
internal_mutex.unlock();
}
/*try_lock()与lock()的功能相似,除了在调用internal_mutex的try_lock()⑦失败时,不能持有对应锁,所以不必更新层级值,并直接返回false。
虽然是运行时检测,但是它没有时间依赖性——不必去等待那些导致死锁出现的罕见条件。同时,设计过程需要去拆分应用,互斥量在这样的情况下可以消除可能导致死锁的可能性。这样的设计练习很有必要去做一下,即使你之后没有去做,代码也会在运行时进行检查。*/
bool try_lock()
{
check_for_hierarchy_violation();
if(!internal_mutex.try_lock()) // 7
return false;
update_hierarchy_value();
return true;
}
};
thread_local unsigned long
hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX); // 7
4. std::unique_lock
std::unique_lock — std::lock_guard
std::unique_lock занимает больше места и немного медленнее, чем std::lock_guard. За гибкость приходится платить, и эта цена позволяет использовать экземпляры std::unique_lock без мьютекса: информация уже сохранена и обновляется.
1. Использование std::lock() и std::unique_lock в операции обмена
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(some_big_object const& sd):some_detail(sd){
}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock); // 1
std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock); // 1 std::def_lock 留下未上锁的互斥量
std::lock(lock_a,lock_b); // 2 互斥量在这里上锁
swap(lhs.some_detail,rhs.some_detail);
}
};
2. Уникальная блокировка может быть добавлена с помощью параметра std::adopt_lock
① Добавление std::try_to_lock
может избежать ненужного ожидания, и он будет определять, может ли текущий мьютекс быть заблокирован.Если он не может быть заблокирован, вы можете сначала выполнить другие коды
#include <iostream>
#include <mutex>
std::mutex mlock;
void work1(int& s) {
for (int i = 1; i <= 5000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
if (munique.owns_lock() == true) {
s += i;
}
else {
// 执行一些没有共享内存的代码
}
}
}
void work2(int& s) {
for (int i = 5001; i <= 10000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
if (munique.owns_lock() == true) {
s += i;
}
else {
// 执行一些没有共享内存的代码
}
}
}
int main()
{
int ans = 0;
std::thread t1(work1, std::ref(ans));
std::thread t2(work2, std::ref(ans));
t1.join();
t2.join();
std::cout << ans << std::endl;
return 0;
}
② Добавление параметра std::defer_lock
означает не блокировать его временно, а затем блокировать вручную, но не разрешается блокировать его перед использованием. Обычно используется с функциями-членам unique_lock для использования
#include <iostream>
#include <mutex>
std::mutex mlock;
void work1(int& s) {
for (int i = 1; i <= 5000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
munique.lock();
s += i;
munique.unlock(); // 这里可以不用unlock,可以通过unique_lock的析构函数unlock
}
}
void work2(int& s) {
for (int i = 5001; i <= 10000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
munique.lock();
s += i;
munique.unlock();
}
}
int main()
{
int ans = 0;
std::thread t1(work1, std::ref(ans));
std::thread t2(work2, std::ref(ans));
t1.join();
t2.join();
std::cout << ans << std::endl;
return 0;
}
Существует также функция-член try_lock, которая имеет ту же функцию, что и параметр try_to_lock выше. Она определяет, можно ли использовать текущую блокировку. Если нет, выполните другие коды и верните false. Если да, заблокируйте и верните true. Код следующее:
void work1(int& s) {
for (int i = 1; i <= 5000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
if (munique.try_lock() == true) {
s += i;
}
else {
// 处理一些没有共享内存的代码
}
}
}
Функция освобождения разрывает соединение между unique_lock и объектом мьютекса и возвращает указатель исходного объекта мьютекса. Если предыдущий мьютекс был заблокирован, вам необходимо вручную разблокировать его позже, код выглядит следующим образом:
void work1(int& s) {
for (int i = 1; i <= 5000; i++) {
std::unique_lock<std::mutex> munique(mlock); // 这里是自动lock
std::mutex *m = munique.release();
s += i;
m->unlock();
}
}
3. Передача права собственности на мьютекс в разных доменах
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk; // 1
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock()); // 2
do_something();
}
5. Альтернативные средства защиты общих данных
1. Обработка условных гонок
Стандартная библиотека C++ предоставляет std::once_flag и std::call_once для обработки условной конкуренции.
Вместо блокировки мьютекса и явной проверки указателя каждый поток должен использовать только std::call_once, и в конце std::call_once он может с уверенностью знать, что указатель был инициализирован другими потоками.
Использование std::call_once потребляет меньше ресурсов, чем явное использование мьютекса, особенно после завершения инициализации.
std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag; // 1
void init_resource()
{
resource_ptr.reset(new some_resource);
}
void foo()
{
std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化
resource_ptr->do_something(); //some_resource初始化完成,可以使用resource_ptr
}
2. Используйте std::call_once в качестве ленивой инициализации членов класса (безопасность потоков).
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(connection_info const& connection_details_):
connection_details(connection_details_)
{
}
void send_data(data_packet const& data) // 1
{
std::call_once(connection_init_flag,&X::open_connection,this); // 2
connection.send_data(data);
}
data_packet receive_data() // 3
{
std::call_once(connection_init_flag,&X::open_connection,this); // 2
return connection.receive_data();
}
};
Вот альтернатива std::call_once, где нужен только один глобальный экземпляр
class my_class;
my_class& get_my_class_instance()
{
static my_class instance; // 线程安全的初始化过程
return instance;
}
Многопоточность может безопасно вызывать функцию get_my_class_instance()①, не беспокоясь о конкуренции данных.
В стандарте C++11: инициализация и определение происходят полностью в одном потоке, и никакой другой поток не может их обработать до завершения инициализации, а условное соревнование заканчивается на инициализации, что намного лучше, чем делать это позже.
3. Вместо использования экземпляра std::mutex для синхронизации лучше использовать boost::shared_mutex для синхронизации
Защита структур данных с помощью boost::shared_mutex
#include <map>
#include <string>
#include <mutex>
#include <boost/thread/shared_mutex.hpp>
class dns_entry;
class dns_cache
{
std::map<std::string,dns_entry> entries;
mutable boost::shared_mutex entry_mutex;
public:
dns_entry find_entry(std::string const& domain) const
{
boost::shared_lock<boost::shared_mutex> lk(entry_mutex); // 1 保护其共享和只读权限
std::map<std::string,dns_entry>::const_iterator const it=
entries.find(domain);
return (it==entries.end())?dns_entry():it->second;
}
void update_or_add_entry(std::string const& domain,
dns_entry const& dns_details)
{
std::lock_guard<boost::shared_mutex> lk(entry_mutex); // 2 独占访问权限
entries[domain]=dns_details;
}
};
4. Стандартная библиотека Nested lock
C++ предоставляет класс std::recursive_mutex.
Вложенные блокировки обычно используются в классах, к которым могут одновременно обращаться несколько потоков, поэтому у них есть мьютекс для защиты данных их членов. Каждая общедоступная функция-член блокирует мьютекс, затем выполняет соответствующую функцию, а затем разблокирует мьютекс.
Разница между lock_guard и unique_lock в С++
- unique_lock может реализовать отложенную блокировку, то есть сначала сгенерировать объект unique_lock, а затем, при необходимости, вызвать функцию блокировки, lock_guard автоматически выполнит операцию блокировки при создании объекта;
- unique_lock может вызывать операцию разблокировки, когда это необходимо, а lock_guard может автоматически разблокировать только после окончания жизненного цикла объекта;