無鎖佇列 自旋鎖佇列 互斥鎖佇列效能對比測試

2021-09-25 17:55:00 字數 3744 閱讀 2784

先大致介紹一下無鎖佇列。無鎖佇列的根本是cas函式——compareandswap,即比較並交換,函式功能可以用c++函式來說明:

int compare_and_swap (

int* reg,

int oldval,

int newval)

它將reg的值與oldval的值進行對比,若相同則將reg賦新值。注意以上操作是原子操作。大部分語言都有提供cas支援,不過函式原型可能有些微的不同,許多語言(包括go)中cas的返回值是標識是否賦值成功的bool值。

無鎖佇列則是以cas來實現同步的一種佇列,我的具體實現這裡就不貼出來了,有點冗長,文末給出了原始碼位址。這裡僅僅大致給出實現思路,網上關於無鎖佇列的資料很多,這裡就不詳細說了。

enqueue

(x)//進佇列改良版

while

(cas

(p.next,

null

, q)

!= true)

;//如果沒有把結點鏈在尾上,再試

cas(tail, oldp, q)

;//置尾結點

}dequeue()

//出佇列

while

(cas

(head, p, p-

>next)

!= true )

;return p-

>next-

>value;

}

自旋鎖是加鎖失敗時接著迴圈請求加鎖,直到成功。它的特點是不會釋放cpu,故也沒有互斥鎖那種核心態切換操作,但缺點也很明顯,就是會一直占用cpu,理論上適用於臨界區小、不需要長時間加鎖的場景。

這裡只貼鎖的相關**,佇列的實現就不貼了:

// 自旋鎖

type spinmutex struct

const locked =

1const unlocked =

0func

(spin *spinmutex)

lock()

}func

(spin *spinmutex)

unlock()

這個沒什麼好說的,用的golang自帶的互斥鎖sync.mutex。

下面將分2種場景進行測試:分別是高併發和低併發。高併發我用4個協程往佇列中push資料,4個協程從佇列中pop資料(雖然不是很高,但足以區分效能,就沒測太高併發了,畢竟測一次等的太久也累);低併發不好模擬,於是我乾脆極端點改為無併發——先順序寫,再順序讀。

大致測試**結構如下(刪減了不關鍵的語句):

t1 := time.

now(

)for i :=

1; i <= datanum; i++

queue.

disable()

for}

fmt.

println

("用時:"

, time.

since

(t1)

)

為了方便對比,我特地還增加了不加鎖的佇列的測試結果。測試結果如下:(左側為datanum資料量)

可以看到資料量小的時候效能差別還不明顯,甚至cas還有少許的優勢。但資料量一大就很明顯的看出自旋鎖的效率會高一點,cas次之。不過它們差別都不大。

這裡用4個生產者4個消費者共用乙個佇列來模擬高併發。測試**結構如下:

func

test()

wgw := sync.waitgroup

t1 := time.

now(

)for i :=

0; i <

4; i++

for i :=

0; i <

4; i++

wgr.

wait()

queue.

disable()

wgw.

wait()

fmt.

println

("用時:"

, time.

since

(t1))}

func

reader

(startnum int

, wg *sync.waitgroup)

} wg.

done()

}func

writter

(wg *sync.waitgroup)

if r == defaultval

} wg.

done()

}

這種情況下就沒法測試無鎖佇列了,資料都不完整(已驗證)。測試結果如下,左側為讀/寫協程數*datanum資料量(下面讀/寫協程數為4指總共開了8個協程):

可以看到cas有巨大的效能優勢,甚至達到了3到5倍的效能差距,說明這個思路還是可行的!(先開始被chan打擊到了)反倒是自旋鎖的效能最差,這個倒有些出乎我的意料,按照我的理解在這種頻繁加鎖解鎖的情況下自旋鎖的效能應該更好才對,若有知情人士望告知。

為了對這幾種鎖的效能特點有更深入的分析,這裡還補充了幾組測試,分別用了不同的協程數和資料量進行補充測試:

可以很明顯的看到乙個趨勢——隨著併發度增加,自旋鎖的效能急劇下降,由無併發時的與cas效能幾乎一樣到最後與cas將近7倍的效率差。而mutex和cas情況下,隨著併發度增加,效能影響並不大,下面將前面的測試資料重新組織一下方便對比:

可以看到總資料量不變的情況下,併發協程數對mutex和cas的影響非常小,基本在波動範圍以內。相較之下自旋鎖就比較慘了。

**根據上面的結果來說的話,當實際競爭特別小的時候,可以考慮用自旋鎖;而併發大的時候,用無鎖佇列這種結構有很大潛在優勢。**之所以說潛在的是因為我也僅僅是簡單的實現某種結構,肯定有考慮不全的地方,我寫這個無鎖例子主要用於測試,也沒打算用於實際場景中。但是我盡量保證了同樣的**結構下,最大化各個鎖結構對效能的影響。總的來說,本文測試結果僅作參考,希望能有拋磚引玉的效果。

針對自旋鎖效率低下的問題我仔細想了想,應該是原子操作cas耗時的問題(畢竟在無併發情況下,cas和真正不加鎖還是有很大的效能差距)。於是對自旋鎖的**進行了微調,減少了cas的呼叫次數:(被注釋掉的是原本的**邏輯)

func

(spin *spinmutex)

lock()

begining:

for spin.mutex != unlocked

if!atomic.

compareandswapint32

(&spin.mutex, unlocked, locked)

}

事實證明,這樣做效率確實提高了約1/4,不過還是改變不了它的大趨勢(與cas和mutex的效能差距依舊巨大),所以就沒更新前面的測試資料了。

不過這也佐證了cas也是比較耗時的乙個操作,平時還是不能肆意使用。

mySQL無鎖佇列 go 無鎖佇列

無鎖佇列適用場景 兩個執行緒之間的互動資料,乙個執行緒生產資料,另外乙個執行緒消費資料,效率高 缺點 需要使用固定分配的空間,不能動態增加 減少長度,存在空間浪費和無法擴充套件空間問題 package main import fmt reflect strings time type loopque...

無鎖環形佇列

環形一讀一寫佇列中,不需要擔心unsigned long溢位問題,因為溢位後自動回歸,相減值還會保留。示例一 注 max count 必須為 2 的指數,即 2,4,8,16.佇列尺寸 define max count 4096 define max mask 4095 max count 1 變數...

KFIFO無鎖佇列

linux核心中實現了以非常漂亮的無鎖佇列,在只有乙個讀者和乙個寫者的情況下,無需上鎖,而擁有執行緒安全的特性,使得效能相比於加鎖方式實現的佇列提公升數倍 kfifo的分析可見 這位作者講的很清楚 kifio可以實現無鎖佇列,但是為什麼可以實現呢,通過這種方式為什麼可以執行緒安全?換句話說,平時實現...