1、先来看一段多线程代码:
std::shared_ptr<some_resource> resource_prtr;
std::mutex resource_mutex;
void foo()
{
std::unique_lock<std::mutex> lk(resource_mutex);
if(!resource_ptr)
{
resource_ptr.reset(new some_resource);
}
lk.unlock();
resource_ptr->so_something();
}
由上一段代码可知,为了确定数据源已经初始化,每个线程都必须等待互斥量,可见没必要。 2、有人改进的双重检测锁模式:
void undefined_behaviour_with_double_checked_locing()
{
if(!resource_ptr)
{
std::lock_guard<std::mutex> lk(resource_mutex);
if(!!resource_ptr)
{
resource_ptr.reset(new some_resource);
}
}
resource_ptr->so_something();
}
但是以上代码存在条件竞争风险:未被锁保护的读取操作1没有与其他线程里被锁保护的写入操作3进行同步,因此就会产生条件竞争。
3、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;
void init_resource()
{
resource_ptr.reset(new some_resource);
}
void foo()
{
std::call_once(resource_flag, init_resource);
resource_ptr->do_something();
}
4 、还有一种是使用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_date(data_packet const& 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();
}
};
}
以上例子中第一次调用send_data()或者receive_data的线程完成线程初始化过程。使用成员函数open_connection()去初始化数据,也需要将this制作穿进去。
5、还有一种初始化过程潜存着条件竞争:其中一个局部变量被声明为static类型,这种变量在声明时候进行初始化;对于多线程调用的初始化,意味着这里有条件竞争-抢着去定义这个变量。在C++11中,该问题已被解决:初始化以及定义,完全在一个线程中发生,并且没有其他线程可在初始化完成前对其进行处理。 更新操作少的情况,可以只在初始化保护数据。
class my_class;
my_class& get_my_class_instance()
{
static my_class instance;
return instance;
}
6、互斥量std::mutex保护数据结构时,可能会削减并发读取数据的可能性。这里需要另一种不同的互斥量,这种互斥量被称为"读者-作者锁",因其允许两种不同的使用方式:一个writer线程独占访问和共享访问,让多个reader线程并发访问。 C++17提供了两种非常好的互斥量—std::shared_mutex和std::shared_time_mutex。C++14只提供了std::shared_timed_mutex,并且在C++11中并未提供任何互斥量类型。其中shared_time_mutex支持更多的操作方式,而shared_mutex拥有更高的性能优势。 可以使用std::shared_mutex进行数据同步:更新操作可以使用std::lock_guardstd::shared_mutex和std::unique_lockstd::shared_mutex上锁;读取操作可以使用std::shared_lockstd::shared_mutex来获取访问权。 唯一限制:当任一线程拥有一个共享锁时,这个线程就去尝试获取一个独占锁;当任一线程拥有一个独占锁,其他线程就无法获得共享锁或者独占锁,知道第一个线程放弃其拥有的锁。一下代码展示了一个简单的DNS缓存:
#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>
class dns_entry;
class dns_cache
{
std::map<std::string, dns_entry> entries;
mutable std::shared_mutex entry_mutex;
public:
dns_entry find_entry(std::string const& domain) const
{
std::shared_lock<std::shared_mutex> lk(entry_mutex);
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<std::shared_mutex> lk(entry_mutex);
entries[domain] = dns_details;
}
};
其中update函数被调用时,独占锁将组织其他线程对数据结构进行修改,并且阻止线程调用find_entry()。
|