1. 程式人生 > >無鎖程式設計和有鎖程式設計效率對比

無鎖程式設計和有鎖程式設計效率對比

主要觀點包括: 程式簡單時,可以通過改程序序結構和鎖粒度,來提高效能 程式複雜時(需要考慮死鎖,優先順序繼承等),可通過無鎖程式設計CAS來提高效能 最近維護的一個網路伺服器遇到效能問題,於是就對原有的程式進行了較大的框架改動。改動最多的是執行緒工作模式與資料傳遞方式,最終的結果是改變鎖的使用模式。經過一番改進,基本上可以做到 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 的偽碼如下:

[cpp] view plaincopyprint?
  1. template <class T>  
  2. bool CAS(T* addr, T expected, T value)   
  3. {  
  4.    if (*addr == expected)   
  5.    {  
  6.       *addr = value;  
  7.       return true;  
  8.    }  
  9.    return false;  
  10. }   
 

      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] ,文獻介紹了一種較好的執行緒間合作的工作模式,當然前提是機器的處理器個數較多,足以支援多組執行緒並行的工作。如果處理器個數較,較多的執行緒之間在各個核心上來回排程增加了系統上下文切換的開銷,會導致系統整體效能下降。

相關推薦

程式設計程式設計效率對比

主要觀點包括: 程式簡單時,可以通過改程序序結構和鎖粒度,來提高效能 程式複雜時(需要考慮死鎖,優先順序繼承等),可通過無鎖程式設計CAS來提高效能 最近維護的一個網路伺服器遇到效能問題,於是就對原有的程式進行了較大的框架改動。改動最多的是執行緒工作模式與資料傳遞方式,最

Java併發程式設計系列之十二 死 飢餓

                        死鎖發生在一個執

刷電機刷電機對比

esc -a 損壞 隨著 這就是 常用 上電 可控 分享 轉自硬件十萬個為什麽 有刷電機工作原理 有刷電機是大家最早接觸的一類電機,中學時物理課堂上介紹電動機也是以它為模型來展示的。有刷電機的主要結構就是定子+轉子+電刷,通過旋轉磁場獲得轉動力矩,從而輸出動能。電刷與換向器

16-golang的快取channel快取channel

  我們先來看看無快取channel   func main() { var channel = make(chan int, 0) go func() { for i := 0; i <= 2; i++ {

符號位移符號位移

轉載:https://blog.csdn.net/BushQiang/article/details/79394211            https://blog.csdn.net/qq_26129689/article/detai

C語言中符號數符號數相加比較的問題

轉自https://blog.csdn.net/supreme42/article/details/6687781 看個題: #include<stdio.h> int main() { unsigned int a=6; int b=-20; printf("%d\n"

怎麼理解界佇列界佇列

有界佇列:就是有固定大小的佇列。比如設定了固定大小的 LinkedBlockingQueue,又或者大小為 0,只是在生產者和消費者中做中轉用的 SynchronousQueue。 無界佇列:指的是沒有設定固定大小的佇列。這些佇列的特點是可以直接入列,直到溢位。

內建(隱式顯示

1.內建鎖: (1)原理:通過內部的一個叫做監視器鎖的原理來實現的,但是監視器鎖本質又是依賴於底層的作業系統的Mutes Lock來實現的,作業系統之間實現執行緒的切換需要從使用者態轉換到核心態,這個成本非常高,狀態之間轉換需要很長的時間,所以內建鎖效率較低。

定義參構造參構造方法

public class h { String name; String sex; int age; public h(){}; //定義無參構造方法 public h(String n,String s,int a){ //定義有參

JavaScript-函式的呼叫,參函式參函式,引數的的傳遞

javascript中的函式分為無參函式和有參函式,無參函式就是函式裡面什麼都別寫,有參函式有變數在裡面。 <!DOCTYPE html> <html> <he

什麼是宣告式(Declarative)程式設計命令式imperative)程式設計

指令式程式設計(imperative):喜歡大量使用可變物件和指令,我們總是習慣於建立物件或者變數,並且修改它們的狀態或者值,或者喜歡提供一系列指令,要求程式執行。 宣告式程式設計(Declarati

宣告式程式設計指令式程式設計

1.一個故事:需要指定詳細的操作內容            小明公司要舉行一次春遊活動,他們老大(主管)告訴小明春遊活動的地點範圍是公司所在市的5A景區,時間是這個雙休日,有25個人。需要定公司的1輛30人的大巴車,在公司集合,出發時間是早上七點半。希望十點能到達景區。之後

mysql 共享(s)排他(x)

  共享鎖:若事務T在A物件上加上共享鎖S,則其他事務不可修改A物件,但是,可以查詢A物件、可以在A物件上加共享鎖(不可加排他鎖(X))      此時,事務T也不可以對A物件進行修改,其他事務在T提交之前也不可以對A物件進行修改。   共享鎖用法:lock in shar

符號整數符號整數比較的注意點

無符號整數和有符號整數比較注意 如果有符號整數是負數,則和無符號整數比較時結果錯誤。 尤其注意陣列的count和一個有符號整數比較這種情況。     NSUInteger x = 1;     NSInteger y = -1;     if(x>y){      

AOP實現原理:從指令式程式設計宣告式程式設計說起

面向方面程式設計(Aspect Oriented Programming,簡稱AOP)是一種宣告式程式設計(Declarative Programming)。宣告式程式設計是和指令式程式設計(Imperative Programming)相對的概念。我們平時使用的程式語言,比

js符號數值符號數值轉化

function UnsignToSign(num){//無符號轉有符號 6553 --> -1 if(num>0xffff/2){ var a=~0xffff; num=num|a; } return num; }  function sign

用鄰接連結串列儲存向圖向圖

上一篇文章我們說到用鄰接矩陣儲存有向圖和無向圖,這種方法的有點是很直觀的知道點與點之間的關係,查詢的複雜度是為O(1)。但是這種儲存方式同時存在著佔用較多儲存空間的問題, 鄰接矩陣儲存很好理解,但是,有時候太浪費空間了,特別是對於頂點數多,但是關聯關係少的圖。 舉個極端

函數語言程式設計響應式程式設計

在程式開發中,a=b+c;賦值之後,b或者c的值變化後,a的值不會跟著變化。響應式程式設計目標就是,如果b或者c的數值發生變化,a的數值會同時發生變化。 函數語言程式設計 函數語言程式設計是一系列被不公平對待的程式設計思想的保護傘,它的核心思想是,它是一

開發式程式設計,宣告式程式設計產生式程式設計(www.mynetweaver.cn)

從概念上看,WebDynpro是基於MVC的宣告式程式設計(declarative programming),也就是面向元資料解析的程式設計。我們可以比較一下幾種常見的程式設計模型,來加深理解。 開發式程式設計是編碼的,如:Java, C#宣告式程式設計是解析的,如:ANT

符號數符號數(一) -- 原碼錶示法補碼錶示法

無符號數: 即沒有符號的數。 在c語言中就是 unsigned 型別的。 無符號數在計算機中的儲存較為簡單, 因為沒有符號位, 直接將數字化成二進位制然後儲存在對應的儲存器或者暫存器中。 這時暫存器或