在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ C/ 3.3 保護(hù)共享數(shù)據(jù)的替代設(shè)施
3.4 本章總結(jié)
6.3 基于鎖設(shè)計(jì)更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)
6.1 為并發(fā)設(shè)計(jì)的意義何在?
5.2 <code>C++</code>中的原子操作和原子類型
A.7 自動(dòng)推導(dǎo)變量類型
2.1 線程管理的基礎(chǔ)
8.5 在實(shí)踐中設(shè)計(jì)并發(fā)代碼
2.4 運(yùn)行時(shí)決定線程數(shù)量
2.2 向線程函數(shù)傳遞參數(shù)
第4章 同步并發(fā)操作
2.3 轉(zhuǎn)移線程所有權(quán)
8.3 為多線程性能設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)
6.4 本章總結(jié)
7.3 對于設(shè)計(jì)無鎖數(shù)據(jù)結(jié)構(gòu)的指導(dǎo)建議
關(guān)于這本書
A.1 右值引用
2.6 本章總結(jié)
D.2 &lt;condition_variable&gt;頭文件
A.6 變參模板
6.2 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)
4.5 本章總結(jié)
A.9 本章總結(jié)
前言
第10章 多線程程序的測試和調(diào)試
5.4 本章總結(jié)
第9章 高級線程管理
5.1 內(nèi)存模型基礎(chǔ)
2.5 識別線程
第1章 你好,C++的并發(fā)世界!
1.2 為什么使用并發(fā)?
A.5 Lambda函數(shù)
第2章 線程管理
4.3 限定等待時(shí)間
D.3 &lt;atomic&gt;頭文件
10.2 定位并發(fā)錯(cuò)誤的技術(shù)
附錄B 并發(fā)庫的簡單比較
5.3 同步操作和強(qiáng)制排序
A.8 線程本地變量
第8章 并發(fā)代碼設(shè)計(jì)
3.3 保護(hù)共享數(shù)據(jù)的替代設(shè)施
附錄D C++線程庫參考
第7章 無鎖并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
D.7 &lt;thread&gt;頭文件
D.1 &lt;chrono&gt;頭文件
4.1 等待一個(gè)事件或其他條件
A.3 默認(rèn)函數(shù)
附錄A 對<code>C++</code>11語言特性的簡要介紹
第6章 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
封面圖片介紹
7.2 無鎖數(shù)據(jù)結(jié)構(gòu)的例子
8.6 本章總結(jié)
8.1 線程間劃分工作的技術(shù)
4.2 使用期望等待一次性事件
8.4 設(shè)計(jì)并發(fā)代碼的注意事項(xiàng)
D.5 &lt;mutex&gt;頭文件
3.1 共享數(shù)據(jù)帶來的問題
資源
9.3 本章總結(jié)
10.3 本章總結(jié)
10.1 與并發(fā)相關(guān)的錯(cuò)誤類型
D.4 &lt;future&gt;頭文件
3.2 使用互斥量保護(hù)共享數(shù)據(jù)
9.1 線程池
1.1 何謂并發(fā)
9.2 中斷線程
4.4 使用同步操作簡化代碼
A.2 刪除函數(shù)
1.3 C++中的并發(fā)和多線程
1.4 開始入門
第5章 C++內(nèi)存模型和原子類型操作
消息傳遞框架與完整的ATM示例
8.2 影響并發(fā)代碼性能的因素
7.1 定義和意義
D.6 &lt;ratio&gt;頭文件
A.4 常量表達(dá)式函數(shù)
7.4 本章總結(jié)
1.5 本章總結(jié)
第3章 線程間共享數(shù)據(jù)

3.3 保護(hù)共享數(shù)據(jù)的替代設(shè)施

互斥量是最通用的機(jī)制,但其并非保護(hù)共享數(shù)據(jù)的唯一方式。這里有很多替代方式可以在特定情況下,提供更加合適的保護(hù)。

一個(gè)特別極端(但十分常見)的情況就是,共享數(shù)據(jù)在并發(fā)訪問和初始化時(shí)(都需要保護(hù)),但是之后需要進(jìn)行隱式同步。這可能是因?yàn)閿?shù)據(jù)作為只讀方式創(chuàng)建,所以沒有同步問題;或者因?yàn)楸匾谋Wo(hù)作為對數(shù)據(jù)操作的一部分,所以隱式的執(zhí)行。任何情況下,數(shù)據(jù)初始化后鎖住一個(gè)互斥量,純粹是為了保護(hù)其初始化過程(這是沒有必要的),并且這會(huì)給性能帶來不必要的沖擊。出于以上的原因,C++標(biāo)準(zhǔn)提供了一種純粹保護(hù)共享數(shù)據(jù)初始化過程的機(jī)制。

3.3.1 保護(hù)共享數(shù)據(jù)的初始化過程

假設(shè)你與一個(gè)共享源,構(gòu)建代價(jià)很昂貴,可能它會(huì)打開一個(gè)數(shù)據(jù)庫連接或分配出很多的內(nèi)存。

延遲初始化(Lazy initialization)在單線程代碼很常見——每一個(gè)操作都需要先對源進(jìn)行檢查,為了了解數(shù)據(jù)是否被初始化,然后在其使用前決定,數(shù)據(jù)是否需要初始化:

std::shared_ptr<some_resource> resource_ptr;
void foo()
{
  if(!resource_ptr)
  {
    resource_ptr.reset(new some_resource);  // 1
  }
  resource_ptr->do_something();
}

當(dāng)共享數(shù)據(jù)對于并發(fā)訪問是安全的,①是轉(zhuǎn)為多線程代碼時(shí),需要保護(hù)的,但是下面天真的轉(zhuǎn)換會(huì)使得線程資源產(chǎn)生不必要的序列化。這是因?yàn)槊總€(gè)線程必須等待互斥量,為了確定數(shù)據(jù)源已經(jīng)初始化了。

清單 3.11 使用一個(gè)互斥量的延遲初始化(線程安全)過程

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);  // 只有初始化過程需要保護(hù) 
  }
  lk.unlock();
  resource_ptr->do_something();
}

這段代碼相當(dāng)常見了,也足夠表現(xiàn)出沒必要的線程化問題,很多人能想出更好的一些的辦法來做這件事,包括聲名狼藉的雙重檢查鎖模式:

void undefined_behaviour_with_double_checked_locking()
{
  if(!resource_ptr)  // 1
  {
    std::lock_guard<std::mutex> lk(resource_mutex);
    if(!resource_ptr)  // 2
    {
      resource_ptr.reset(new some_resource);  // 3
    }
  }
  resource_ptr->do_something();  // 4
}

指針第一次讀取數(shù)據(jù)不需要獲取鎖①,并且只有在指針為NULL時(shí)才需要獲取鎖。然后,當(dāng)獲取鎖之后,指針會(huì)被再次檢查一遍② (這就是雙重檢查的部分),避免另一的線程在第一次檢查后再做初始化,并且讓當(dāng)前線程獲取鎖。

這個(gè)模式為什么聲名狼藉呢?因?yàn)檫@里有潛在的條件競爭,未被鎖保護(hù)的讀取操作①?zèng)]有與其他線程里被鎖保護(hù)的寫入操作③進(jìn)行同步。因此就會(huì)產(chǎn)生條件競爭,這個(gè)條件競爭不僅覆蓋指針本身,還會(huì)影響到其指向的對象;即使一個(gè)線程知道另一個(gè)線程完成對指針進(jìn)行寫入,它可能沒有看到新創(chuàng)建的some_resource實(shí)例,然后調(diào)用do_something()④后,得到不正確的結(jié)果。這個(gè)例子是在一種典型的條件競爭——數(shù)據(jù)競爭,C++標(biāo)準(zhǔn)中這就會(huì)被指定為“未定義行為”。這種競爭肯定是可以避免的??梢蚤喿x第5章,那里有更多對內(nèi)存模型的討論,包括數(shù)據(jù)競爭的構(gòu)成。

C++標(biāo)準(zhǔn)委員會(huì)也認(rèn)為條件競爭的處理很重要,所以C++標(biāo)準(zhǔn)庫提供了std::once_flagstd::call_once來處理這種情況。比起鎖住互斥量,并顯式的檢查指針,每個(gè)線程只需要使用std::call_once,在std::call_once的結(jié)束時(shí),就能安全的知道指針已經(jīng)被其他的線程初始化了。使用std::call_once比顯式使用互斥量消耗的資源更少,特別是當(dāng)初始化完成后。下面的例子展示了與清單3.11中的同樣的操作,這里使用了std::call_once。在這種情況下,初始化通過調(diào)用函數(shù)完成,同樣這樣操作使用類中的函數(shù)操作符來實(shí)現(xiàn)同樣很簡單。如同大多數(shù)在標(biāo)準(zhǔn)庫中的函數(shù)一樣,或作為函數(shù)被調(diào)用,或作為參數(shù)被傳遞,std::call_once可以和任何函數(shù)或可調(diào)用對象一起使用。

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);  // 可以完整的進(jìn)行一次初始化
  resource_ptr->do_something();
}

在這個(gè)例子中,std::once_flag①和初始化好的數(shù)據(jù)都是命名空間區(qū)域的對象,但是std::call_once()可僅作為延遲初始化的類型成員,如同下面的例子一樣:

清單3.12 使用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();
  }
};

例子中第一個(gè)調(diào)用send_data()①或receive_data()③的線程完成初始化過程。使用成員函數(shù)open_connection()去初始化數(shù)據(jù),也需要將this指針傳進(jìn)去。和其在在標(biāo)準(zhǔn)庫中的函數(shù)一樣,其接受可調(diào)用對象,比如std::thread的構(gòu)造函數(shù)和std::bind(),通過向std::call_once()②傳遞一個(gè)額外的參數(shù)來完成這個(gè)操作。

值得注意的是,std::mutexstd::one_flag的實(shí)例就不能拷貝和移動(dòng),所以當(dāng)你使用它們作為類成員函數(shù),如果你需要用到他們,你就得顯示定義這些特殊的成員函數(shù)。

還有一種情形的初始化過程中潛存著條件競爭:其中一個(gè)局部變量被聲明為static類型。這種變量的在聲明后就已經(jīng)完成初始化;對于多線程調(diào)用的函數(shù),這就意味著這里有條件競爭——搶著去定義這個(gè)變量。在很多在前C++11編譯器(譯者:不支持C++11標(biāo)準(zhǔn)的編譯器),在實(shí)踐過程中,這樣的條件競爭是確實(shí)存在的,因?yàn)樵诙嗑€程中,每個(gè)線程都認(rèn)為他們是第一個(gè)初始化這個(gè)變量線程;或一個(gè)線程對變量進(jìn)行初始化,而另外一個(gè)線程要使用這個(gè)變量時(shí),初始化過程還沒完成。在C++11標(biāo)準(zhǔn)中,這些問題都被解決了:初始化及定義完全在一個(gè)線程中發(fā)生,并且沒有其他線程可在初始化完成前對其進(jìn)行處理,條件競爭終止于初始化階段,這樣比在之后再去處理好的多。在只需要一個(gè)全局實(shí)例情況下,這里提供一個(gè)std::call_once的替代方案

class my_class;
my_class& get_my_class_instance()
{
  static my_class instance;  // 線程安全的初始化過程
  return instance;
}

多線程可以安全的調(diào)用get_my_class_instance()①函數(shù),不用為數(shù)據(jù)競爭而擔(dān)心。

對于很少有更新的數(shù)據(jù)結(jié)構(gòu)來說,只在初始化時(shí)保護(hù)數(shù)據(jù)。在大多數(shù)情況下,這種數(shù)據(jù)結(jié)構(gòu)是只讀的,并且多線程對其并發(fā)的讀取也是很愉快的,不過一旦數(shù)據(jù)結(jié)構(gòu)需要更新,就會(huì)產(chǎn)生競爭。

3.3.2 保護(hù)很少更新的數(shù)據(jù)結(jié)構(gòu)

試想,為了將域名解析為其相關(guān)IP地址,我們在緩存中的存放了一張DNS入口表。通常,給定DNS數(shù)目在很長的一段時(shí)間內(nèi)保持不變。雖然,在用戶訪問不同網(wǎng)站時(shí),新的入口可能會(huì)被添加到表中,但是這些數(shù)據(jù)可能在其生命周期內(nèi)保持不變。所以定期檢查緩存中入口的有效性,就變的十分重要了;但是,這也需要一次更新,也許這次更新只是對一些細(xì)節(jié)做了改動(dòng)。

雖然更新頻度很低,但更新也有可能發(fā)生,并且當(dāng)這個(gè)可緩存被多個(gè)線程訪問,這個(gè)緩存就需要處于更新狀態(tài)時(shí)得到保護(hù),這也為了確保每個(gè)線程讀到都是有效數(shù)據(jù)。

沒有使用專用數(shù)據(jù)結(jié)構(gòu)時(shí),這種方式是符合預(yù)期,并且為并發(fā)更新和讀取特別設(shè)計(jì)的(更多的例子在第6和第7章中介紹)。這樣的更新要求線程獨(dú)占數(shù)據(jù)結(jié)構(gòu)的訪問權(quán),直到其完成更新操作。當(dāng)更新完成,數(shù)據(jù)結(jié)構(gòu)對于并發(fā)多線程訪問又會(huì)是安全的。使用std::mutex來保護(hù)數(shù)據(jù)結(jié)構(gòu),顯的有些反應(yīng)過度(因?yàn)樵跊]有發(fā)生修改時(shí),它將削減并發(fā)讀取數(shù)據(jù)的可能性)。這里需要另一種不同的互斥量,這種互斥量常被稱為“讀者-作者鎖”,因?yàn)槠湓试S兩種不同的使用方式:一個(gè)“作者”線程獨(dú)占訪問和共享訪問,讓多個(gè)“讀者”線程并發(fā)訪問。

雖然這樣互斥量的標(biāo)準(zhǔn)提案已經(jīng)交給標(biāo)準(zhǔn)委員會(huì),但是C++標(biāo)準(zhǔn)庫依舊不會(huì)提供這樣的互斥量[3]。因?yàn)榻ㄗh沒有被采納,這個(gè)例子在本節(jié)中使用的是Boost庫提供的實(shí)現(xiàn)(Boost采納了這個(gè)建議)。你將在第8章中看到,這種鎖的也不能包治百病,其性能依賴于參與其中的處理器數(shù)量,同樣也與讀者和作者線程的負(fù)載有關(guān)。為了確保增加復(fù)雜度后還能獲得性能收益,目標(biāo)系統(tǒng)上的代碼性能就很重要。

比起使用std::mutex實(shí)例進(jìn)行同步,不如使用boost::shared_mutex來做同步。對于更新操作,可以使用std::lock_guard<boost::shared_mutex>std::unique_lock<boost::shared_mutex>上鎖。作為std::mutex的替代方案,與std::mutex所做的一樣,這就能保證更新線程的獨(dú)占訪問。因?yàn)槠渌€程不需要去修改數(shù)據(jù)結(jié)構(gòu),所以其可以使用boost::shared_lock<boost::shared_mutex>獲取訪問權(quán)。這與使用std::unique_lock一樣,除非多線程要在同時(shí)獲取同一個(gè)boost::shared_mutex上有共享鎖。唯一的限制:當(dāng)任一線程擁有一個(gè)共享鎖時(shí),這個(gè)線程就會(huì)嘗試獲取一個(gè)獨(dú)占鎖,直到其他線程放棄他們的鎖;同樣的,當(dāng)任一線程擁有一個(gè)獨(dú)占鎖時(shí),其他線程就無法獲得共享鎖或獨(dú)占鎖,直到第一個(gè)線程放棄其擁有的鎖。

如同之前描述的那樣,下面的代碼清單展示了一個(gè)簡單的DNS緩存,使用std::map持有緩存數(shù)據(jù),使用boost::shared_mutex進(jìn)行保護(hù)。

清單3.13 使用boost::shared_mutex對數(shù)據(jù)結(jié)構(gòu)進(jìn)行保護(hù)

#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;
  }
};

清單3.13中,find_entry()使用boost::shared_lock<>來保護(hù)共享和只讀權(quán)限①;這就使得多線程可以同時(shí)調(diào)用find_entry(),且不會(huì)出錯(cuò)。另一方面,update_or_add_entry()使用std::lock_guard<>實(shí)例,當(dāng)表格需要更新時(shí)②,為其提供獨(dú)占訪問權(quán)限;update_or_add_entry()函數(shù)調(diào)用時(shí),獨(dú)占鎖會(huì)阻止其他線程對數(shù)據(jù)結(jié)構(gòu)進(jìn)行修改,并且阻止線程調(diào)用find_entry()。

3.3.3 嵌套鎖

當(dāng)一個(gè)線程已經(jīng)獲取一個(gè)std::mutex時(shí)(已經(jīng)上鎖),并對其再次上鎖,這個(gè)操作就是錯(cuò)誤的,并且繼續(xù)嘗試這樣做的話,就會(huì)產(chǎn)生未定義行為。然而,在某些情況下,一個(gè)線程嘗試獲取同一個(gè)互斥量多次,而沒有對其進(jìn)行一次釋放是可以的。之所以可以,是因?yàn)?code>C++標(biāo)準(zhǔn)庫提供了std::recursive_mutex類。其功能與std::mutex類似,除了你可以從同一線程的單個(gè)實(shí)例上獲取多個(gè)鎖?;コ饬挎i住其他線程前,你必須釋放你擁有的所有鎖,所以當(dāng)你調(diào)用lock()三次時(shí),你也必須調(diào)用unlock()三次。正確使用std::lock_guard<std::recursive_mutex>std::unique_lock<std::recursive_mutex>可以幫你處理這些問題。

大多數(shù)情況下,當(dāng)你需要嵌套鎖時(shí),就要對你的設(shè)計(jì)進(jìn)行改動(dòng)。嵌套鎖一般用在可并發(fā)訪問的類上,所以其擁互斥量保護(hù)其成員數(shù)據(jù)。每個(gè)公共成員函數(shù)都會(huì)對互斥量上鎖,然后完成對應(yīng)的功能,之后再解鎖互斥量。不過,有時(shí)成員函數(shù)會(huì)調(diào)用另一個(gè)成員函數(shù),這種情況下,第二個(gè)成員函數(shù)也會(huì)試圖鎖住互斥量,這就會(huì)導(dǎo)致未定義行為的發(fā)生?!白兺ǖ摹苯鉀Q方案會(huì)將互斥量轉(zhuǎn)為嵌套鎖,第二個(gè)成員函數(shù)就能成功的進(jìn)行上鎖,并且函數(shù)能繼續(xù)執(zhí)行。

但是,這樣的使用方式是不推薦的,因?yàn)槠溥^于草率,并且不合理。特別是,當(dāng)鎖被持有時(shí),對應(yīng)類的不變量通常正在被修改。這意味著,當(dāng)不變量正在改變的時(shí)候,第二個(gè)成員函數(shù)還需要繼續(xù)執(zhí)行。一個(gè)比較好的方式是,從中提取出一個(gè)函數(shù)作為類的私有成員,并且讓其他成員函數(shù)都對其進(jìn)行調(diào)用,這個(gè)私有成員函數(shù)不會(huì)對互斥量進(jìn)行上鎖(在調(diào)用前必須獲得鎖)。然后,你仔細(xì)考慮一下,在這種情況調(diào)用新函數(shù)時(shí),數(shù)據(jù)的狀態(tài)。


[3] Howard E. Hinnant, “Multithreading API for C++0X—A Layered Approach,” C++ Standards Committee Paper N2094, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2094.html.