自旋鎖和快取一致性

2021-06-21 16:58:52 字數 3212 閱讀 3493

class taslock 

}void unlock()

}

atomicboolean

的getandset

方法是原子的,

tas迴圈執行

getandset

方法,當發現原來的值為

false

,且設定新值為

true

成功後,則認為獲取到了鎖。

class ttaslock

if(!state.getandset(true))}}

void unlock()

}

ttas與tas在功能的正確性上是一致的,但在實現手法上的區別在於,ttas首先迴圈呼叫state.get()方法,只有當返回結果為false,即此時該鎖還沒有被其他執行緒占有時,才會再去呼叫getandset去獲取鎖

基於tas和ttas設計鎖,然後用多執行緒(10執行緒併發)累加乙個公共的計數器。

public class mytest 

});}

}private void add()

system.out.println(system.currenttimemillis() - starttime);

system.exit(0);

}

在我的機器上(4核)執行多次,統計得到的大致結果對比如下:

tas耗時:平均4362ms左右

ttas耗時:平均3895ms左右

單執行緒耗時:平均26ms左右

分析:可以看出無論是tas還是ttas,效能均遠遠差於單執行緒的版本,這是合理的,因為並行是否能帶來效能的提公升,關鍵取決於執行的任務中序列任務所佔的比例。而本例中,由於鎖的存在,使得序列所佔整個任務的比例為100%,因此即使執行緒再多,也只能序列執行,而多執行緒帶來的上下文切換又會消耗一定的時間,所以多執行緒的效能更差。

但是,為什麼ttas會比tas的效能要好呢?ttas是迴圈執行state.get()方法,tas是迴圈執行state.getandset(true)方法,這兩種實現手法帶來的本質區別是什麼?要解釋這一點,就必須先了解現代多處理器的快取架構

所有的處理器共享乙個主存(memory),主存的訪問速度很慢,需要消耗50~100個cycles,每個處理器都需要通過同乙個bus同memory和其他處理器通訊,而同一時刻只允許乙個處理器通過bus傳送訊息(但memory和所有的處理器都能同時監聽過bus的訊息)。由於memory訪問速度慢,而且還可能會等待,所以每個處理器都有自己的區域性快取(cache)。cache的訪問速度很快,只需要消耗1~2個cycles,而且不會出現等待。

但天下沒有免費的晚餐,現在同一主存位址對應的資料可能會在多個cache中儲存,任何乙個處理器都可能會修改這份資料,那麼如何保證cache之間、cache與memory之間的資料的一致性呢?由快取一致性協議來保證,mesi協議就是其中最著名的一種。

mesi協議將cache line分為四種狀態:

modified:該cache line已經被修改,且只被乙個處理器的cache快取,最終必須寫回主存;

exclusive:該cache line 只被乙個處理器的cache快取,未被修改,與主存中的資料一致。

shared:該cache line被多個處理器的cache快取,未被修改,與主存中的資料一致

invalid:該cache line已經失效

下面以乙個圖例解釋mesi協議的執行:

a)        處理器a在bus上發出讀取位址a處的資料的訊息。由於只有主存有該資料,所以主存監聽到該訊息後,將位址a處的資料給處理器a,處理器a將資料快取到自己的cache中,cache line的狀態為exclusive;

b)        處理器b在bus上發出讀取同樣位址a處的資料,處理器a監聽到了該訊息,將之前快取的位址a處的資料給處理器b。此時,cache line的狀態都變為shared;

c)        處理器b修改位址a處的資料,將自己cache中的cache line的狀態修改為modified。同時,在bus上發出位址a處的資料被修改的訊息,處理器a監聽到該訊息,將自己cache中cache line的狀態修改為invalid;

d)   處理器a在bus上發出重新讀取位址a處的資料的訊息,處理器b監聽到該訊息後,將修改後的資料同時發給處理器a和主存。處理器a和b的cache中的cache line的狀態都變為shared。

在tas中,每次執行鎖的getandset(true)方法,會獨佔bus以傳送鎖的state變數被修改的訊息,導致其他處理器執行getandset(true)時都需要等待,出現了序列。更重要的是,有可能此時占有鎖的處理器正準備執行set(false)方法以釋放鎖,但由於bus一直被執行getandset(true)方法的處理器占用著,導致釋放鎖也被延遲。

而在ttas中,只有當get()方法返回false的情況下,才會執行getandset(true)方法。只要state沒有被修改,處理器都在各自的cache中區域性自旋著,不會占用bus,因此不會造成鎖的釋放被延遲。

當然,ttas也不是最完美的,當處理器釋放鎖執行set(false)方法後,所有其他處理器的cache都會被失效,無法再繼續區域性自旋,呼叫get()方法都會占用bus,出現序列。當get()方法返回false後,此時,可能就會有多個處理器正試圖同時執行getandset(true)方法,又導致了和tas一樣的情況出現。這一輪過後,各處理器又會在各自的cache中區域性自旋,直到鎖再次被釋放。

現在的問題焦點是:只要執行了一次無效的getandset(true)方法,都會導致所有處理器的cache被失效,失效後get()方法的呼叫就會占用bus,有可能導致處理器釋放鎖被延遲。如果能降低無效的呼叫次數,則能提公升ttas的速度。假設在第一輪中,有3個處理器同時執行getandset(true)方法,那麼只有乙個處理器能夠獲取到鎖,另外的兩個處理器又會重新恢復到ttas的區域性自旋中。當第乙個處理器將鎖釋放後,這兩個處理器又可能會同時執行getandset(true)方法,那麼不可避免的又會存在乙個處理器執行一次無效的getandset(true)方法呼叫。如果每個處理器在每次執行無效的getandset(true)方法呼叫後,都休眠一段隨機的時間,再進入到ttas的區域性自旋中,則多個處理器同時執行getandset(true)方法呼叫的概率就能被降低。

資料庫和快取一致性

今天程式過程中突然想到了乙個問題,怎麼保證redis快取和mysql資料庫中的資料相同 一致性 即在更新資料時怎樣保證資料庫和redis快取始終相同。從理論上講,給快取設定過期時間是保證最終一致性的解決方案。這種方案下,所有寫操作以資料庫為準,對快取做最大努力即可。下面介紹的是不依賴於給快取設定過期...

快取一致性

一般應用而言,追求的都是快取的最終一致性。一般的快取系統,都是按照key去快取查詢,如果不存在對應的value,就應該去後端系統查詢 比如db 如果key對應的value是一定不存在的,並且對該key併發請求量很大,就會對後端系統造成很大的壓力。這就叫做快取穿透。引起這個問題的主要原因還是高併發的時...

快取一致性

計算機體系結構量化研究方法 第五版 學習筆記 快取一致性 1 快取一致性的問題 2 儲存器一致性的概念 3 一致性的基本實現方案 大型 多級快取可以充分降低處理器對儲存頻寬的需求。採用對稱共享儲存器的計算機通常支援對共享資料與專用資料的快取。多處理器之間的通訊基本上是通過讀寫共享資料實現。為了降低訪...