最近維護的一個網路伺服器遇到效能問題,於是就對原有的程式進行了較大的架構改動。改動最多的是線程工作模式與資料傳遞方式,最終的結果是改變鎖的使用模式。經過一番改進,基本上可以做到
GMb
網卡全速工作處理。在
效能達標之後,一度在想有沒有什麼辦法使用更加輕量級鎖,或者去掉鎖的使用,為此搜尋一些相關的研究成果,並做了一些實驗來驗證這些成果,因而就有這篇文章。希望有做類似工作的同行可以有所借鑒。如果有人也有相關的經驗,歡迎和我交流。
1
無鎖編程概述
本節主要對文獻
[1]
進行概括,做一些基礎知識的介紹。
所謂有鎖編程,就是當你需要共用資料的時候,你需要有序的去訪問,所有改變共用資料的操作都必須表現出原子的語義,即便是像
++k
,這種操作也需要使用鎖進行。有鎖編程面臨時效率的下降、死結、優先順序反轉等問題,都需要設計者小心的進行最佳化和解決。本文並不對這三個問題進行討論。
在無鎖編程中,並不是說所有操作都是原子的,只有一個很有限的操作集是原子的,這就意味著無鎖編程十分困難。那麼這個有限的操作集是否存在,存在的話包含哪些原子操作呢?
2003
年
Maurice Herlihy
的一篇論文
”Wait-Free Synchronization”[3]
解決了這個問題。這裡給出文章的結論,文章指出像
test-and-set,swap,fetch-and-add
甚至是原子隊列對於多線程而言都無法做到
lock-free
。而最樸素最簡單的原語
CAS(compare-and-swap)
操作即可以完成所有的無鎖功能,其他的如
LL/SC (load linked/store
conditional)
。
CAS
的偽碼如下:
template <class T><br />bool CAS(T* addr, T expected, T value)<br />{<br /> if (*addr == expected)<br /> {<br /> *addr = value;<br /> return true;<br /> }<br /> return false;<br />}<br />
CAS
將
expected
與一個記憶體位址進行比較,如果比較成功,就將記憶體內容替換為
new
。當前大多數機器都在硬體級實現了這個操作,在
Inter
處理器上這個操作是
CMPXCHG
,因而
CAS
是一個最基礎的原子操作。
wait-free / lock-free
與
有鎖對比
wait-free
的過程可以通過有限步驟完成,而不管其他線程的速度。
lock-free
的過程保證至少一個線程在執行,其他線程可能會被延遲,但系統整體仍在前進。
有鎖的情況下,如果某個線程佔有鎖,則其他線程就無法執行。更普通的,有鎖需要避免死結和活鎖的情況。
2
無鎖編程的相關研究與進展
本節內容對文獻
[2]
進行概述,介紹當前已經實現的無鎖演算法與資料結構。
近二十年來研究者們對
lock-free
和
wait-free
的演算法和資料結構進行了大量的研究。實現了一些
wait-free
和
lock-free
的演算法,比如
FIFO
的隊列和
LIFO
的棧,而更複雜的最佳化級隊列、
hash
表及紅/黑樹狀結構的
lock-free
演算法也漸漸為人所知。
無鎖演算法的實現都依賴記憶體屏障,因而具有平台相關性。下面將列舉目前已經較為成熟的原子操作和演算法資料結構的實現。
- MidiShare Source
Code is available under the GPL license. MidiShare includes
implementations of lock-free FIFO queues and LIFO stacks.
- Appcore
is an SMP and HyperThread friendly library which uses
Lock-free techniques to implement stacks, queues, linked lists and other
useful data structures. Appcore appears currently to be for x86 computers
running Windows. The licensing terms of Appcore are extremely unclear.
- Noble
– a library of
non-blocking synchronisation protocols. Implements lock-free stack, queue,
singly linked list, snapshots and registers. Noble is distributed under a
license which only permits non-commercial academic use.
- lock-free-lib
published under the GPL license. Includes implementations of software
transactional memory, multi-workd CAS primitives, skip lists, binary
search trees, and red-black trees. For Alpha, Mips, ia64, x86, PPC, and
Sparc.
- Nonblocking
multiprocessor/multithread algorithms in C++ (for MSVC/x86) posted by
Joshua Scholar to musicdsp.org
, and are
presumably in the public domain. Included are queue, stack,
reference-counted garbage collection, memory allocation, templates for
atomic algorithms and types. This code is largely untested. A local mirror
is here
.
- Qprof
includes the Atomic_ops library of atomic operations and data structures
under an MIT-style license. Only available for Linux at the moment, but
there are plans to support other platforms. download available here
- Amino Concurrent
Building Blocks provides lock free datastructures and STM for C++ and
Java under an Apache Software (2.0) licence.
其中
Noble
已經進行了商業化,
License
相當不便宜。
3
效能分析
本節對
PTHREAD
中的
mutex
,
windows
中的原子增,及
CAS
原子操作進行對比,並對
MidiShare
中實現的無鎖
FIFO
隊列與基於
STL
的
list
實現的有鎖隊列進行的效能對比和分析,並對最佳化方式進行了總結。
3.1
原子增的效能測試
測試機
CPU
為
Intel E5300 2.60GHZ
首先是對簡單的遞增操作進行了測試,分別對無任何同步機制的
++
操作、
pthread_mutex
保護的
++
操作,以及
CAS
的語義實現的
atomic_add1()
以及
windows
下的
interlockedIncrease()
進行了單個線程情況下的定量測試。
i++ |
3.2 億 |
lock(p_mutex);i++;unlock(p_mutex); |
2 千萬 |
CAS_atomic_add1(i) |
4 千萬 |
interlockedIncrease(&i) |
4 千萬 |
首先在無任何同步情況下,
CPU
可以每秒執行
++
操作
3.2
億次,接近於
CPU
的主頻速率。而每次
++
時執行
thread_mutex_lock()
及
unlock()
操作情況下,
CPU
每秒只能執行
2
千萬次,這就是說
CPU
每秒鐘可以執行加鎖及解鎖操作共
4
千萬次,加解鎖的開銷是執行加法指令的的
15
倍左右。而
CAS
的情況稍好,為每秒
4
千萬次。這個速度與
windows
下的
interlockedIncrease()
的執行速度十分近似。
從上面的測試結果來看,
windows
下的原子增操作與
CAS
實現的增操作代價基本是相同的,估計
windows
底層也是藉助彙編指令
CMPXCHG
的
CAS
來實現原子增操作的。當然
pthread_mutex
作為一種互斥鎖,也是擁有相當高的效率的,在沒有鎖突然的情況下,加鎖開銷與一次
CAS
的開銷相當。
但如果對比無同步的
++
操作,硬體級的同步也造成了至少
8
倍的效能下降。
接著,對
pthread_mutex
的程式進行了邏輯最佳化,分別測試了
++
執行
8
次、
20,100
次進行一次加解鎖的情況。
lock();for(k=0;k<8;i++,k++);unlock() |
1.2 億 |
lock();for(k=0;k<20;i++,k++);unlock() |
2 億 |
lock();for(k=0;k<100;i++,k++);unlock() |
3.4 億 |
結果
CPU
每秒鐘可以執行
++
的次數為
1.2
億
/2
億
/3.4
億,這種情況與預期是一致的,因為每秒鐘調用加解鎖的次數分別是原來的
1/8
、
1/20
和
1/100
,當執行
100
次
++
進行一次加解鎖後,效能已經達到了無任何同步時的效能。當然原子的
interlockedIncrease()
和
CAS
實現的
atomic_add1()
都不具備這種批量處理的改進優勢,無論如果,它們最好的執行情況已經固定了。
對於在單線程與多線程的情況下的
windows
下的原子操作的效能測試情況,可以參考文獻
[4]
,這裡只列出其中的結論。其所列的測試機
CPU
為
Intel2.66GHZ
雙核處理器。
單個線程執行
2
百萬次原子增操作
interlockedIncrease |
78ms |
Windows CriticalSection |
172ms |
OpenMP 的 lock 操作 |
250ms |
兩個線程對共用變數執行
2
百萬次原子增操作
interlockedIncrease |
156ms |
Windows CriticalSection |
3156ms |
OpenMP 的 lock 操作 |
1063ms |
3.2
無鎖隊列與有鎖隊列的效能測試
這裡測試的無鎖列隊由
MidiShare
實現的,而有鎖隊列是通過
pthread_mutex
與
c++
的
STL list
共同實現。這裡只列出測試結果。
對於儲存相同的資料的情況下,從主線程
enque
並從子線程
deque
,計算每秒鐘
enque/deque
的次數,當然二者基本上是相同的。
無鎖隊列的效能在
150w -200w
次入隊操作,這個效能已經無法再有任何提高,因為每次入隊出隊操作都是硬體級的互斥。而對於有鎖隊列,根據每次加解鎖之間處理入隊的次數的不同,有以下的結果:
lock();for(k=0;k<x;i++,k++);unlock() |
結果(次/s) |
x=1 |
40 萬 |
x=10 |
190 萬 |
x=128 |
350 萬 |
x=1000 |
400 萬 |
x=10000 |
396 萬 |
這說明通過對鎖之間的資料進行批處理,可以極大的提高系統的效能,而使用原子操作,則無法實現批處理上的改進。
4
結論
通過上面的無鎖和有鎖的效能測試,可以得出這樣的結論,對於
CAS
實現的硬體級的互斥,其單次操作效能比相同條件下的應用程式層的較為高效,但當多個線程並發時,硬體級的互斥引入的代價與應用程式層的鎖爭用同樣令人惋惜。因此如果純粹希望通過使用
CAS
無鎖演算法及相關資料結構而帶來程式效能的大量提升是不可能的,硬體級原子操作使應用程式層操作變慢,而且無法再度最佳化。相反通過對有鎖多線程程式的良好設計,可以使程式效能沒有任何下降,可以實現高度的並發性。
但是我們也要看到應用程式層無鎖的好處,比如不需要程式員再去考慮死結、優先順序反轉等棘手的問題,因此在對應用程式不太複雜,而對效能要求稍高時,可以採用有鎖多線程。而程式較為複雜,效能要求滿足使用的情況下,可以使用應用級無鎖演算法。
至於如何對多線程的工作模式進行更好的調度,可以參考文獻
[5]
,文獻介紹了一種較好的線程間合作的工作模式,當然前提是機器的處理器個數較多,足以支援多組線程並行的工作。如果處理器個數較,較多的線程之間在各個核心上來回調度增加了系統環境切換的開銷,會導致系統整體效能下降。
參考文獻
[1] Lock-Free Data Structures
http://www.drdobbs.com/184401865
[2] Some notes on lock-free
wait-free algorithms http://www.rossbencina.com/code/lockfree
[3] Wait-Free Synchronization http://www.podc.org/dijkstra/2003.html
[4] OpenMP
建立線程中的鎖及原子操作效能比較
http://blog.163.com/kangtao-520/blog/static/772561452009510751068/
[5]
多核編程中的線程分組競爭模式
http://kangtao-520.blog.163.com/blog/static/77256145200951074121305/