3.3 保护共享数据的替代设施(C++并发编程实战)

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>可以帮你处理这些问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值