生活随笔
收集整理的這篇文章主要介紹了
深入理解并行编程-分割和同步设计(四)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
原文鏈接??? 作者:paul??? 譯者:謝寶友,魯陽,陳渝
圖1.1:設計模式與鎖粒度
圖1.1是不同程度同步粒度的圖形表示。每一種同步粒度都用一節內容來描述。下面幾節主要關注鎖,不過其他幾種同步方式也有類似的粒度問題。
1.1. 串行程序
圖1.2:Intel處理器的MIPS/時鐘頻率變化趨勢
如果程序在單處理器上運行的足夠快,并且不與其他進程、線程或者中斷處理程序發生交互,那么您可以將代碼中所有的同步原語刪掉,遠離它們所帶來的開銷和復雜性。好多年前曾有人爭論摩爾定律最終會讓所有程序都變得如此。但是,隨著2003年以來Intel CPU的CPU MIPS和時鐘頻率增長速度的停止,見圖1.2,此后要增加性能,就必須提高程序的并行化程度。關于是否這種趨勢會導致一塊芯片上集成幾千個CPU的爭論不會很快停息,但是考慮到Paul是在一臺雙核筆記本上敲下這句話的,SMP的壽命極有可能比你我都長。另一個需要注意的地方是以太網的帶寬持續增長,如圖1.3所示。這種增長會導致多線程服務器的產生,這樣才能有效處理通信載荷。
圖1.3:以太網帶寬 v.s. Intel x86處理器的性能
請注意,這并不意味這您應該在每個程序中使用多線程方式編程。我再一次說明,如果一個程序在單處理器上運行的很好,那么您就從SMP同步原語的開銷和復雜性中解脫出來吧。圖1.4中哈希表查找代碼的簡單之美強調了這一點。
| 07 | struct node **buckets; |
| 19 | int hash_search(struct hash_table *h, long key) |
| 25 | cur = h->buckets[key % h->nbuckets]; |
| 29 | if (cur->key >= key) { |
| 31 | return (cur->key == key); |
圖1.4:串行版的哈希表搜索算法
1.2. 代碼鎖
代碼鎖是最簡單的設計,只使用全局鎖。在已有的程序上使用代碼鎖,可以很容易的讓程序可以在多處理器上運行。如果程序只有一個共享資源,那么代碼鎖的性能是最優的。但是,許多較大且復雜的程序會在臨界區上執行許多次,這就讓代碼鎖的擴展性大大受限。
| 09 | struct node **buckets; |
| 21 | int hash_search(struct hash_table *h, long key) |
| 31 | cur = h->buckets[key % h->nbuckets]; |
| 35 | if (cur->key >= key) { |
| 37 | retval = (cur->key == key); |
| 39 | spin_unlock(&hash_lock); |
| 49 | spin_unlock(&hash_lock); |
圖1.5:基于代碼鎖的哈希表搜索算法
因此,您最好在只有一小段執行時間在臨界區程序,或者對擴展性要求不高的程序上使用代碼鎖。這種情況下,代碼鎖可以讓程序相對簡單,和單線程版本類似,如圖1.5所示。但是,和圖1.4相比,hash_search()從簡單的一行return變成了3行語句,因為在返回前需要釋放鎖。
圖1.6:鎖競爭
并且,代碼鎖尤其容易引起“鎖競爭”,一種多個CPU并發訪問同一把鎖的情況。照顧一群小孩子(或者像小孩子一樣的老人)的SMP程序員肯定能馬上意識到某樣東西只有一個的危險,如圖1.6所示。
該問題的一種解決辦法是下節描述的“數據鎖”。
1.3. 數據鎖
| 04 | struct bucket **buckets; |
| 08 | spinlock_t bucket_lock; |
| 17 | int hash_search(struct hash_table *h, long key) |
| 23 | bp = h->buckets[key % h->nbuckets]; |
| 24 | spin_lock(&bp->bucket_lock); |
| 27 | if (cur->key >= key) { |
| 28 | retval = (cur->key == key); |
| 29 | spin_unlock(&bp->hash_lock); |
| 34 | spin_unlock(&bp->hash_lock); |
圖1.7:基于數據鎖的哈希表搜索算法
許多數據結構都可以分割,數據結構的每個部分帶有一把自己的鎖。這樣雖然每個部分一次只能執行一個臨界區,但是數據結構的各個部分形成的臨界區就可以并行執行了。在鎖競爭必須降低時,和同步開銷不是主要局限時,可以使用數據鎖。數據鎖通過將一塊過大的臨界區分散到各個小的臨界區來減少鎖競爭,比如,維護哈希表中的per-hash-bucket臨界區,如圖1.7所示。不過這種擴展性的增強帶來的是復雜性的提高,增加了額外的數據結構struct bucket。
圖1.8:數據鎖
和圖1.6中所示的緊張局面不同,數據鎖帶來了和諧,見圖1.8——在并行程序中,這總是意味著性能和可擴展性的提升。基于這種原因,Sequent在它的DYNIX和DYNIX/ptx操作系統中大量使用了數據鎖[BK85][Inm85][Gar90][Dov90][MD92][MG92][MS93]。
不過,那些照顧過小孩子的人可以證明,再細心的照料也不能保證一切風平浪靜。同樣的情況也適用于SMP程序。比如,Linux內核維護了一種文件和目錄的緩存(叫做“dcache”)。該緩存中的每個條目都有一把自己的鎖,但是對應根目錄的條目和它的直接后代相較于其他條目更容易被遍歷到。這將導致許多CPU競爭這些熱門條目的鎖,就像圖1.9中所示的情景。
圖1.9:數據鎖出現問題
在許多情況下,可以設計算法來減少數據沖突的次數,某些情況下甚至可以完全消滅沖突(像Linux內核中的dcache一樣[MSS04])。數據鎖通常用于分割像哈希表一樣的數據結構,也適用于每個條目用某個數據結構的實例表示這種情況。2.6.17內核的task list就是后者的例子,每個任務結構都有一把自己的proc_lock鎖。
在動態分配結構中,數據鎖的關鍵挑戰是如何保證在已經獲取鎖的情況下結構本身是否存在。圖1.7中的代碼通過將鎖放入靜態分配并且永不釋放的哈希桶,解決了上述挑戰。但是,這種手法不適用于哈希表大小可變的情況,所以鎖也需要動態分配。在這種情況,還需要一些手段來阻止哈希桶在鎖被獲取后這段時間內釋放。
小問題1.1:當結構的鎖被獲取時,如何防止結構被釋放呢?
1.4. 數據所有權
數據所有權方法按照線程或者CPU的個數分割數據結構,這樣每個線程/CPU都可以在不需任何同步開銷的情況下訪問屬于它的子集。但是如果線程A希望訪問另一個線程B的數據,那么線程A是無法直接做到這一點的。取而代之的是,線程A需要先與線程B通信,這樣線程B以線程A的名義執行操作,或者,另一種方法,將數據遷移到線程A上來。
數據所有權看起來很神秘,但是卻應用得十分頻繁。
1. 任何只能被一個CPU或者一個線程訪問的變量(C或者C++中的auto變量)都屬于這個CPU或者這個進程。
2. 用戶接口的實例擁有對應的用戶上下文。這在與并行數據庫引擎交互的應用程序中十分常見,這讓并行引擎看起來就像順序程序一樣。這樣的應用程序擁有用戶接口和他當前的操作。顯式的并行化只在數據庫引擎內部可見。
3. 參數模擬通常授予每個線程一段特定的參數區間,以此達到某種程度的并行化。
如果共享比較多,線程或者CPU間的通信會帶來較大的復雜性和通信開銷。不僅如此,如果使用的最多的數據正好被一個CPU擁有,那么這個CPU就成為了“熱點”,有時就會導致圖1.9中的類似情況。不過,在不需要共享的情況下,數據所有權可以達到理想性能,代碼也可以像圖1.4中所示的順序程序例子一樣簡單。最壞情況通常被稱為“尷尬的并行化”,而最好情況,則像圖1.8中所示一樣。
另一個數據所有權的重要實例發生在數據是只讀時,這種情況下,所有線程可以通過復制來“擁有”數據。
1.5鎖粒度與性能
本節以一種數學上的同步效率的視角,將視線投向鎖粒度和性能。對數學不敢興趣的讀者可以跳過本節。
本節的方法是用一種關于同步機制效率的粗略隊列模型,該機制只操作一個共享的全局變量,基于M/M/1隊列。M/M/1隊列。
(本節未翻譯)
總結
以上是生活随笔為你收集整理的深入理解并行编程-分割和同步设计(四)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。