你想鎖定,你想等待。因此,某些地方應該有「條件」(類Unix系統上的pthread_cond_t
)。
我建議如下:
- 有一個全局mutex其僅用於在地圖上添加或刪除鍵。
- 映射將鍵映射到值,其中值是包裝器。每個包裝都包含一個條件並可能包含一個值。該值在設置時發出信號。
當線程希望從緩存中獲取值時,它首先獲取全局互斥鎖。然後它會在地圖:
- 如果該鍵的包裝,而且包裝包含一個值,那麼線程有其價值,並可以釋放全局互斥鎖。
- 如果這個鍵有一個包裝,但沒有值,那麼這意味着一些其他線程正忙於計算值。線程然後阻塞條件,當線程完成時被其他線程喚醒。
- 如果沒有包裝,則線程在地圖中註冊一個新包裝,然後繼續計算值。計算該值時,它會設置該值並指示條件。
在僞代碼,這看起來是這樣的:
mutex_t global_mutex
hashmap_t map
lock(global_mutex)
w = map.get(key)
if (w == NULL) {
w = new Wrapper
map.put(key, w)
unlock(global_mutex)
v = compute_value()
lock(global_mutex)
w.set(v)
signal(w.cond)
unlock(global_mutex)
return v
} else {
v = w.get()
while (v == NULL) {
unlock-and-wait(global_mutex, w.cond)
v = w.get()
}
unlock(global_mutex)
return v
}
在pthreads
而言,lock
是pthread_mutex_lock()
,unlock
是pthread_mutex_unlock()
,unlock-and-wait
是pthread_cond_wait()
和signal
是pthread_cond_signal()
。 unlock-and-wait
原子地釋放互斥並將線程標記爲等待條件;當線程喚醒時,互斥鎖會自動重新獲取。
這意味着每個包裝必須包含一個條件。這體現了您的各種要求:
- 沒有線程長時間保持互斥(阻塞或計算值)。
- 當要計算一個值時,只有一個線程執行該操作,其他希望訪問該值的線程只等待它可用。
注意,當一個線程希望得到一個值,並發現一些其他線程已經忙於計算它的線程最終鎖定全局互斥鎖兩次:在開始一次,一旦當值可用。一個更復雜的解決方案,每個包裝一個互斥體,可能會避免第二次鎖定,但除非爭用非常高,否則我懷疑這是值得的。
關於有很多互斥體:互斥體便宜。一個互斥體基本上是一個int
,它只不過是用來存儲它的四個左右的RAM字節。當心Windows術語:在Win32中,我稱之爲互斥體的互鎖區域被視爲「互鎖區域」;當調用CreateMutex()
時,Win32創建的東西是完全不同的,它可以從幾個不同的進程訪問,並且由於涉及到內核的往返行爲,因此成本更高。請注意,在Java中,每個對象實例都包含一個互斥量,而Java開發人員似乎並不太在這個主題上過分暴躁。
也許第一句話說明了這一點,但我並不清楚併發限制是否適用於讀取同一對象。換句話說,兩個線程可以同時使用現有的密鑰嗎? – 2010-01-26 14:05:07
是的,如果已經存在於緩存中,則2個線程應同時讀取相同的密鑰。 否則,如果該鍵不存在,則一個線程必須對該鍵進行鎖定,而第二個線程必須等待另一個線程完成計算並將該對象添加到緩存中。在此期間,其他線程被允許從其他鍵讀取。 這可以通過緩存的多讀/單寫互斥來完成;當被鎖定寫入時,新的條目將被添加到緩存中。你可以把它看作一個元組。密鑰互斥鎖被鎖定,然後釋放緩存互斥鎖。 –
2010-01-26 15:05:34