3.3.1 保护共享数据的初始化过程
互斥量是最通用的机制,但其并非保护共享数据的唯一方式。这里有许多替代方案可以在特定的情况下,提供更合适的保护。
延迟初始化在单线程中很常见——每一个操作都需要先对源进行检查,为了了解数据是否被初始化,然后再其使用前决定,数据是否需要初始化:
std::shared_ptr<some_resource> resource_ptr;
void foo()
{
if(!resource_ptr)
{
resource_ptr.reset(new some_resource); //1
}
resource_ptr->do_something();
}
上述1中转为多线程代码时需要保护的,如下使用mutex使线程资源产生不必要的序列化,等待互斥量:
std::shared_ptr<some_resource> resource_ptr;
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->do_something();
}
C++标准中提供了std::once_flag和std::call_once来处理条件竞争。比起锁住互斥量,并显示的检查指针,每个线程只需要使用std::call_once,在std::call_once结束时,就能安全的知道指针已经被其他线程初始化了。使用std::call_once比显示使用互斥量消耗的资源更少,特别是当初始化完成后。
下面展示了和上述用mutex同样的操作,这里使用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();
}
这个例子,std::call_once和初始化好的数据都是命名空间区域的对象,但是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);
return connection.receive_data();
}
};
3.3.2 保护很少更新的数据结构
对于将域名解析成相关ip地址,我们在缓存中存放了一张DNS入口表:给定DNS数目在很长一段时间内保持不变,新的入口可能被添加到表中,但是这些数据可能在生命周期内保持不变,所以需要定期检查缓存中入口有效性就变得十分重要。虽然更新频率很低,但是更新还是会发生。
为了确保数据有效性:更新要求数据独占数据结构的访问权,读的时候需要并发访问是安全的,使用std::mutex粒度太大,可以使用读写锁——boost::shared_mutex(boost库,准C++标准),允许一个线程独占访问和共享访问,让多个读者线程并发访问。读写锁依赖处理器数量,同样也与读者和写者线程的负载有关。
如下就是展示了一个简单的DNS缓存,使用std::map持有缓存数据,使用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 //1
{
boost::shared_lock<boost::shared_mutex> lk(entry_mutex);
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(std::string const domain,
dns_entry const& dns_details)
{
std::lock_guard<boost::shared_mutex> lk(entry_mutex); //2
entries[domain] = dns_details;
}
};
find_entry()使用boost::shared_lock<>来保护共享和只读权限;这使得多线程可以同时调用find_entry(),且不会出错。另一方面,update_or_add_entry使用std::lock_guard<>,当表格更新的时候,为其提供独占访问权限。update_or_add_entry函数调用的时候,独占锁会阻止其他线程对数据结构进行修改,并且阻止线程调用find_entry()。
3.3.3 嵌套锁
对于嵌套锁std::recursive_mutex而言,可以从同一线程的单个实例获取多个锁。互斥量锁住前,你必须释放你拥有的锁,当你调用lock()三次,你也必须调用unlock()三次。正确使用std::lock_guard<std::recursive_mutex>和std::unique<std::recursive_mutex>可以帮你处理这些问题。