<var id="k9x2y"></var><li id="k9x2y"><object id="k9x2y"><cite id="k9x2y"></cite></object></li>
<progress id="k9x2y"></progress>
      <button id="k9x2y"><acronym id="k9x2y"></acronym></button>
        <button id="k9x2y"><acronym id="k9x2y"><cite id="k9x2y"></cite></acronym></button>
      1. <rp id="k9x2y"></rp>
        1. <button id="k9x2y"><acronym id="k9x2y"></acronym></button>
        2. Windows下輕量級Semaphore快速信號量的實現

            最近在重寫彈幕君的正式版本,打算加入多線程支持,遇到了不少同步問題。這里就其中一個線程同步的信號量性能問題開一篇日志記錄一下吧~

          前言

            關于信號量、互斥體和臨界區等用于同步的對象,這里就不再贅述了,因為無論是網上還是書本上,關于同步的知識絕對會提到這幾個熟悉的名詞。關于這幾個同步對象本來是沒什么可以挑剔的,邏輯上和實際使用上應用多很廣。但是問題是Windows下實現這些對象的方法有些過于低效,因此這篇日志就是想討論一下如何配合一些原子操作手段而提高Windows下Semaphore等同步對象的方法。
            總所周知(我想找到這篇日志的人都會知道),Windows下Mutex、Semaphore和Event都是內核對象,在撰寫代碼的時候都是以HANDLE關鍵字來聲明對象的,不同的只是在申請(創建)對象的時候調用需要的CreateXXX()方法罷了。但是每次操作內核對象,都必然花費數千個CPU周期來調度一次所有內核對象,顯然在低爭用的情況下是非常不值得的。而與這三者不同的是CRITICALSECTION(臨界區),在進入臨界區的時候,并沒有馬上交給內核分配,而是通過本用戶線程檢查是否有其他線程爭用。如果不存在爭,則本線程立即獲取臨界資源;如果臨界資源已被別的線程占用了,則進入內核,讓系統控制線程狀態(如果有設置自旋的話,則在進入內核之前還讓CPU“空轉”等待資源釋放)。這就臨界區高效的原因了。
            臨界區的高性能是出于它用戶級的鎖定檢查,而同為同步對象的信號量和互斥體為什么不能擁有臨界區的高效呢?這正是本文要探討的內容了。

          實現

            作為實現的基礎,臨界區的實現的設計思想完全符合我們信號量的需求,但是臨界區并不等于信號量,顯然不能照單全收。在我看來,臨界區本質就是信號量≤1的信號量對象的特例。因此,只需簡單地為臨界區“加上”信號數量就可以了。

          基本設計思想
          FastSemaphore.Wait():
          1.如果信號量>0,則跳到(4)
          2.如果在自旋中信號量>0,跳到(4)
          3.進入內核,等待系統調度
          4.信號量-1

          FastSemaphore.Post():
          1.信號量+N
          2.如果信號量≤0,則返回
          3.釋放N個內核對象

            實現原理就是這么簡單,但是實際編寫代碼可不能按照單線程的思想實現啦。要知道,每個Semaphore將在一個或多個線程中被使用,也就是信號量作為多個線程共享對象應該保證線程安全。那么可能有人會想,直接用EnterCriticalSection()來保護整個Semaphore不久好了?–答案是否定的,因為這樣做的話,Semaphore就變成了臨界資源,也就是說,只能有一個線程能夠在某個時刻訪問這個信號量,因此當任意一個線程被信號量阻塞之后,其余的線程將再也不能訪問這個信號量–也就是傳說中的死鎖。
            那么如果不能簡單地把整個信號量保護起來,那么應該如何保證我們這個快速Semaphore的線程安全呢?–答案就是原子操作了。在實現的主要問題是,多個線程在對信號量的值檢查和修改的時候很容易會讀到“臟”數據,因此,使用原子的讀、寫、加、減等操作就能完美快速地實現這個模型了。
            下面貼一下關鍵代碼:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          
          //檢查信號量
          //FastSemaphore.TryWait():
          for(long tmpCount = semCount;true; tmpCount = semCount){
              if(tmpCount < = 0)   //信號量不可用時
                  return false;   //獲取失敗
              //檢查信號量是否臟數據并自減信號量
              if(InterlockedCompareExchange(
                 &semCount, tmpCount - 1, tmpCount) == tmpCount)
                  break;  //獲取成功
          }
              return true;
           
          //等待信號量
          //FastSemaphore.Wait():
          for(unsigned i = 0; i < spinCount; i++) //先自旋
              if(TryWait())
                  return;
           
          if(InterlockedDecrement(&semCount) < 0) //semCount--
              WaitForSingleObject(sem, INFINITE); //進入內核等待
           
          //釋放信號量
          //FastSemaphore.Post():
          //增加N個信號量
          if(InterlockedExchangeAdd(&semCount, n) < 0)
                  ReleaseSemaphore(sem, 1, NULL);  //只釋放一次

          完整代碼
          點擊下載: FastSemaphore.h
          Ps.由于代碼比較短及性能原因,類函數都寫成內聯函數并放在頭文件FastSemaphore.h中了,使用時只需包含這個頭文件就可以了。

          后記

            上文提到“使用臨界區保護整個信號量對象”顯然是不行的,但是我覺得使用臨界區去保護信號量的值來達到線程安全目的倒是應該可以的。但是在我寫的上一個版本,使用臨界區實現的快速信號量卻沒有達到目的,對信號量的訪問依然是不能保證一致性的。我覺得這個設計方向應該是沒有問題的,大概是我的技術還遠沒到家才實現不了吧~

          關于自旋
            自旋是一種對多處理器相當有效的機制,而在單處理器非搶占式的系統中基本上沒有作用。一次只允許一個CPU執行核心代碼并發性不夠高,若期望核心程序在多CPU之間的并行執行,將核心分為若干相對獨立的部分,不同的CPU可以同時進入和執行核心中的不同部分,實現時可以令每個相對獨立的區域進行自旋。

            • adadas
            • 2015/10/02 3:58上午

            這個代碼都錯的離譜了。WaitForSingleObject會把信號量減一,你的sem和semCount的值不一致

          1. 暫無 Trackback

          ?

          return top

          小明看看