[轉載]基於效率考慮,對Windows多線程同步機制的選擇,分析與實測

來源:互聯網
上載者:User

連結:http://waterwood.blog.163.com/blog/static/43596554200793033955/

聲明:以下內容轉載自新帆:nntp://news.newsfan.net 新聞群組:電腦.軟體.編程.VisualStudio
作者:愛果斯坦,評註為水木所加,轉載請註明出處。

        最近,對在一段代碼中是用CriticalSection還是Interlocked***拿不定主意,正好我也想對這個問題有個定量的瞭解,於是做了些測試,總結成下面這篇文章。希望有所助益。

        首先要明確一點:同步器的相對效率,是在雖然進行多線程同步,但並沒有發生“衝突”的前提下而言的。就是說,一個線程獨佔了一項資源,隨後很快就釋放了,而在此期間並沒有其它線程也試圖訪問該資源而進入等待狀態——真正需要進行等待的情況,發生機率必須很小。良好的多線程設計必須保證這一點,否則如果你的幾個線程總是我等你、你等我,說明設計上有很嚴重的問題(這樣嚴重的問題幾乎可以視為Bug)。如果它們常常在同一時刻只能有一個在運行,那要多線程有什麼意思?還不如用單線程,起碼節省同步的時間。

        現在來看CriticalSection。雖然,它內部使用了Event,但並不是每次都用。第一個成功進入CriticalSection的線程就根本不需要使用核心對象。雖然不能看到原始碼,但我考慮過,如果我來設計,我會怎樣做,而且我相信,這也正是Windows中實際所做的(真要實現起來,還會有更多細節問題需要考慮,我這裡只提一些要點,如有興趣,可以試試自己實現一個CriticalSection,也是個不錯的練習):
        1.使用InterlockedExchange(或者某個相關函數),在CRITICAL_SECTION結構裡儲存本線程的ID號。
        2.如果原來的ID號是NULL,或者就是本線程ID(別忘了CriticalSection是允許同一線程重複進入的),那麼調用就成功了。
        3.如果進入CriticalSection失敗,則本線程的ID號已經存入,直接用WaitForSingleObject等待內含的Event即可(因此,只有進入等待的時候,才需要使用核心級的等待函數。實際上,需要令儲存的線程ID構成一個鏈表,以便應付多個線程同時進入等待的情況)。
        4.成功進入CriticalSection的線程在離開的時候,再次使用InterlockedExchange恢複所儲存的線程ID。如果發現儲存的線程ID不是NULL也不是自己,就說明另外一個線程在等待中,調用一次SetEvent即可(但要注意,這也是一個慢速得多的函數,好在它只有在存在等待線程的時候才需要)。

        在MSDN中,對Windows2000新增的函數InitializeCriticalSectionAndSpinCount的說明裡,所透露的一些細節也可以成為我的以上推測的佐證。

        最後,要完全解決這個問題,需要瞭解一下386以上組合語言,再綜合MSDN和《Windows核心編程》中得來的星點知識。我的估計如下:

       ◎最快的當然是C語言的加減運算,它們直接對應簡單的機器指令;
       ◎Interlocked****等函數其次,它們對應的指令其實也很簡單(起碼在x86架構上是這樣,通常只需1~3個指令)。但是,它們需要CPU放棄通常的指令最佳化,還可能需要通過系統匯流排通知其它CPU(CPU內部的一級、二級緩衝,直到記憶體都可能牽連到)。我估計,這會減慢速度5~10倍;
      ◎CrititalSection應該與Interlocked****相當,只是稍微慢一點。因為它實際上調用前者。
      ◎其它如互斥器、訊號器之類最慢,我的估計是也許比CrititalSection慢幾十甚至100倍。因為它們需要切換到核心模式,再切換回來,這需要執行大量指令(在X86上看上去指令不多,但這些指令一個就對應RISC類型的CPU,如Alpha上的一個子程式,一個指令就是幾十個刻度)。也因此,互斥器等核心對象本身的速度差別是微不足道的,沒有必要考慮,因為核心模式切換才是效率瓶頸。

      以下是我的實測結果:

The system performance counter's frequency is:3579545 Repeat times for each test is:1000000

General ++/-- operators: t1(++ only)=0.00371197, t2(-- only)=0.00372959

Both time=0.00754256, t1+t2-both:-0.000101004, NET TIME COST:0.00754256
Interlocked(In/De)crement:0.115052

Critical section:0.140891

Mutex:1.47912

General ++/-- operators: t1(++ only)=0.00376848, t2(-- only)=0.0037205
Both time=0.00756789, t1+t2-both:-7.89067e-005, NET TIME COST:0.00756789
Interlocked(In/De)crement:0.114687
Critical section:0.141372
Mutex:1.49058

General ++/-- operators: t1(++ only)=0.00371739, t2(--
only)=0.00380439
Both time=0.00749592, t1+t2-both:2.58552e-005, NET TIME
COST:0.00747007
Interlocked(In/De)crement:0.114999
Critical section:0.14113
Mutex:1.47861

General ++/-- operators: t1(++ only)=0.00379342, t2(--
only)=0.00371602
Both time=0.00754006, t1+t2-both:-3.06324e-005, NET TIME
COST:0.00754006
Interlocked(In/De)crement:0.114092
Critical section:0.141929
Mutex:1.48663

General ++/-- operators: t1(++ only)=0.00380472, t2(--
only)=0.00377649
Both time=0.00752712, t1+t2-both:5.40851e-005, NET TIME
COST:0.00747303
Interlocked(In/De)crement:0.113746
Critical section:0.140481
Mutex:1.48115

      測試程式是一個單線程命令列程式,使用VC2005編譯,Release版,無調試資訊、優先為速度最佳化。運行時,不考慮其它線程,只管用一個線程反覆鎖定與釋放資源,看其執行速度。實測用的機器是P4
2.4G(有3、4年的較舊機器了),每種調用重複一百萬次,輪迴測試了5趟。第一行打出的performance
frequency沒什麼大用,這是硬體相關的值,說明我用的這個平台上的計時精度而已。後面每趟測試中給出的就是跑完迴圈所用的時間,單位為秒。其中的++和--運算就是基本的C運算子,但作用目標是volatile
LONG型(如果不加上volatile,編譯器最佳化會把整個迴圈都省略掉,而且,這也就不成為對多線程環境的測試了)。
      開始先只做++和--,然後在每趟迴圈中++和--各執行一次,前兩次的時間之和,再減後一個的差值應該就是空迴圈的時間,所以在隨後所有測試的結果中,一般都會扣除空迴圈所需的時間。但不幸這個時間太微小,甚至有時會成為負數(這時我就忽略該項)。其它迴圈都是每次兩項調用——一增一減,或者一鎖一放。

     結論:我的估計大體沒什麼意外,但在具體的速度比值上,就有些出入。實際看來:InterlocdedIncrement和InterlockedDecrement的速度是++和--的大約16分之一(可能因為我忘了考慮函數調用時,入棧出棧和指令跳轉的開銷)。CriticalSection只比前者慢大約25~30%,這點差異通常幾乎可以忽略。這是最接近我的估計的,也說明我對其實現方式的推測正確。Criticalsection的技術本質,就是基於Interlocked****,從而以幾乎相同的時間代價完成更複雜的同步需要。但也不要忘了,在現實中,如果在可以用Interlocked****處理的簡單數值上動用Critical
section,往往用一個Interlocked****單獨調用能解決的事就需要進入和離開Criticalsection的兩次調用,所以實際差異恐怕還得加倍。另一方面,任何需要同步處理比單個數值更複雜情況的時候,Interlocked****就肯定不值得考慮(而且幾乎肯定根本就無法適用)。最後,互斥器比我的估計快很多,只比前者慢10倍(充分證明我對彙編編程的瞭解都是紙上談兵,呵呵)。當然,一個數量級的差距在編程時仍然是不可忽略的差異。所以,核心同步器應該盡量只在跨進程等迫不得已的情況下加以利用。

      評註:

      多線程、多進程編程尤其是多線程編程在實際應用中有著重要的意義,尤其是在網路通訊,圖形影像處理,工控自動化以及B/S開發等領域。對於線程及線程同步的恰當使用可以作為衡配量序員基本素質的標準之一。關於多線程我將會專門撰文介紹,這裡只對作者的觀點做幾點說明:

    1.在多線程、多進程的應用程式中,程式的執行效率具有非常重要的意義,尤其是資料輸送量大,或者服務終端數多的服務程式。

    2.線上程、進程同步的代碼中,一個基本的原則是:在保證邏輯正確的前提下,盡量縮小同步代碼的範圍,不然的話,就會失去多線程的優勢。根據傳統的觀點,線程式服務的代碼應當盡量保證其“局限性”,也就是說,線程中的代碼應當緊湊、迴圈、簡潔,充分利用CPU的“指令預測”能力,這樣最大的好處是,如果CPU數目增加,能夠使服務效能實現“線性”增長。

    3.對於單個資料的同步操作來說,最簡單的同步修改方法是InterlockedIncreament,InterlockedDecreament,InterlockedExchange等同步化的資料修改函數,效率最高。

    4.臨界區同步適合約步稍大範圍的代碼,效率也很高,但僅能實作類別似於WaitForSingle和WaitAll的邏輯。

    5.如果要實現更為複雜的同步,則應採用Event,Mutex等系統級同步對象結合WaitForSingleObject(Ex)(),WaitForMultipleObjects(Ex)()等API實現 WaitForSingle,WaitAll,WaitAny等邏輯。此時的程式執行效率要比前二者低1個數量級,所以合理分配代碼同步地區至關重要。

   6.除了C++能夠調用這些系統API,相應的同步機制在java,.net中都有類似的封裝實現(.net中的實現實際上就是對系統API的封裝調用)。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.