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>可以幫你處理這些問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章