第8章 用戶模式下的線程同步(1)_Interlocked系列函數


8.1 原子訪問:Interlocked系列函數(Interlock英文為互鎖的意思)

(1)原子訪問的原理

  ①原子訪問:指的是一線程在訪問某個資源的同時,能夠保證沒有其他線程會在同一時刻訪問該資源。

  ②從匯編的角度看,哪怕很簡單的一條高級語言都可以被編譯成多條的機器指令。在多線程環境下,這條語句的執行就可能被打斷。而在打斷期間,其中間結果可能己經被其他線程更改過,從而導致錯誤的結果。

  ③在Intelx86指令體系中,有些運算指令加上lock前綴就可以保證該指令操作的原子性。其原理是CPU執行該指令時發現其前面加lock前綴,就會在總線維持一個硬件信號以阻止其他CPU(或線程)訪問與該指令相同的目標內存地址。(注意是指令目標操作數的內存地址而且這些地址是經過內存對齊過的

  ④以InterlockedIncrement函數為例分析原子訪問的原理

LONG InterlockedIncrement(LPLONG volatile lpAddend)
{
   _asm{                      //xadd指令兩個功能:①交換(opd)→(ops);②(opd)←(opd)+(ops)
         mov  eax,1           //前綴lock表示線程在執行該指令時,會將[ecx]內存鎖
         mov cx,Addend        //定(實際上是在CPU總線上放一個信號)以標志該內存正在被使用,
         lock xadd [ecx],eax  //從而阻止其他線程同時訪問該內存。即其他線程要么在該指令之前,
         inc eax              //要么在指令之后才能訪問[ecx]指向的這塊內存
        }
}    

(2)Windows內核支持的整數原子操作——Interlocked***互鎖函數

函數

描述

InterlockedIncrement

InterlockedDecrement

對LONG變量加(減)1,如:InterlockedIncrement(&g_iX)

(內部使用lock xadd指令)

InterlockedExchangeAdd

將一個值加到一個LONG變量,返回變量原值,使用lock xadd指令,如:int g_iX = 0;   //

InterlockedExchangeAdd(&g_iX,-2); //g_iX -= 2;

InterlockedCompareExchange

InterlockedCompareExchange( plDest,lExchange,

lComperand)。比如*plDestination==lComperand,如果相等將*plDest修改為lExchange,如果不等,則*plDest不變。返回值為*plDest原來的值。 (使用lock cmpxchag指令)

InterlockedExchange

InterlockedExchangePointer

將第1個參數所指的內存里的當前值,以原子方式替換為第2個參數指定的值。函數返回值為原始值。后面那個函數是改變一個指針本身的值。(如果xchg指令,雖不加lock。但默認為原子操作)

InterlockedOr

對一個LONG變量做邏輯或運算,使用lock or指令

InterlockedAnd

對一個LONG變量做邏輯與運算,使用lock and指令

InterlockedXor

對一個LONG變量做邏輯異或運算,使用lock xor指令

(3)Interlocked單向鏈表函數

函數

描述

InitializeSListHead

創建一個空棧

InterlockedPushEntrySList

在棧頂添加一個元素

InterlockedPopentrySList

移除位於棧頂的元素並將它返回

InterlockedFlushSlist

清空棧

QueryDepthSlist

返回棧中元素的數量

【Interlocked單鏈表】演示程序

#include <windows.h>
#include <malloc.h>
#include <stdio.h>
#include <tchar.h>

typedef struct _tag_PROGRAM_ITEM
{
    SLIST_ENTRY  ItemEntry;
    ULONG Signature;
}PROGRAM_ITEM,*PPROGRAM_ITEM;

int _tmain()
{
    ULONG Count;
    PSLIST_ENTRY pFirstEntry, pListEntry;
    PSLIST_HEADER  pListHead;
    PPROGRAM_ITEM pProgramItem;

    //初始化鏈表頭部
    //C庫函數_aigned_malloc用來分配一個塊對齊過的內存,其中第1個參數表示要分配
    //的字節數,第2個參數表示要對齊到的字節邊界(必須是2的整數冪次方)
    pListHead = (PSLIST_HEADER)_aligned_malloc(sizeof(SLIST_HEADER),
                                               MEMORY_ALLOCATION_ALIGNMENT);
    if (NULL == pListHead){
        _tprintf(_T("內存分配失敗!"));
        return -1;
    }

    //初始化鏈表頭,創建一個空棧
    InitializeSListHead(pListHead);

    //插入10個元素
    for (Count = 1; Count <= 10;Count++){
        pProgramItem = (PPROGRAM_ITEM)_aligned_malloc(sizeof(PROGRAM_ITEM), 
                                              MEMORY_ALLOCATION_ALIGNMENT);
        if (NULL == pProgramItem){
            _tprintf(_T("內存分配失敗!"));
            return -1;
        }
        pProgramItem->Signature = Count;

        //返回值為鏈表中以上的第1個元素,如果以前是空鏈表,則這里返回NULL
        pFirstEntry = InterlockedPushEntrySList(pListHead, 
                                    &(pProgramItem->ItemEntry)); //也可以pProgramItem
    }

    //刪除10個元素,並顯示其signature字段
    for (Count = 10; Count >= 1;Count-=1){
        pListEntry = InterlockedPopEntrySList(pListHead);
        if (NULL == pListEntry){
            _tprintf(_T("鏈表是空的!"));
            return -1;
        }
        pProgramItem = (PPROGRAM_ITEM)pListEntry;
        _tprintf(_T("Signature = %d\n"), pProgramItem->Signature);

        //釋放該元素的內存
        _aligned_free(pListEntry);
    }

    //清空鏈表,並驗證是否所有元素都己釋放
    pListEntry = InterlockedFlushSList(pListHead);
    pFirstEntry = InterlockedPopEntrySList(pListHead);
    if (pFirstEntry != NULL){
        _tprintf(_T("錯誤:鏈表非空!\n"));
        return -1;
    }

    _aligned_free(pListHead);

    _tsystem(_T("PAUSE"));
    return 0;
}

(4)利用InterlockedExchange實現自旋鎖(spinlock)

 //全局變量用來表明“共享資源”是否正在被使用
 BOOl g_fResourceInUse = FALSE;

 void Func()
{
      //等待資源的訪問權——注意InterlockedExchange返回舊的值
      while(InterlockedExchange(&g_fResourceInUse,TRUE)==TRUE)
         Sleep(0); //如果等不到鎖,就休眼一下,以防止循環,浪費CPU。
     
      //訪問資源
      ……

      //不再使用資源時
      InterlockedExchange(&g_fResource,FALSE); //交出鎖
}

  ①使用這項技術要極其小心,因為旋轉鎖是通過循環實現的,較耗費CPU時間,所以在while中加Sleep,可以改善這種狀況,以避免浪費CPU。當然也可以用SwitchToThread代替,以便讓低優先級的線程也有被調度的機會。

  ②特別要注意的是,在單CPU的機器上要避免使用旋轉鎖,因為如果這個線程一直在不停循環,對CPU浪費大,也影響了其他線程改變鎖的值,造成惡性循環。

  ③使用旋轉鎖的線程優先級要相同,否則如果等待鎖的線程優先級高,則使用資源的線程可能會因分配不到CPU時間而無法釋放鎖。所以使用這種鎖的線程要通過SetProcessPrirityBoost(或SetThreadPriortyBoost)來禁用系統動態提升線程優先級

  ④要確保鎖變量和鎖所保護的數據位於不同的高速緩存行(cache line),如果在同一高速緩存行,當使用資源的CPU更改了被保護的數據,會也會其他CPU相應的高速緩存行失效,這里等待鎖的CPU還要從內存注意:不是CPU高速緩存行)讀入鎖的狀態,這浪費了CPU時間。

  ⑤旋轉鎖是假定被保護資源始終只會占用一小段時間。與切換到內核模式的等待相比,這種通過循環方式的等待效率更高

 

8.2 高速緩存行

8.2.1 CPU、CPU高速緩存、內存的關系——見《深入理解計算機系統(第2版)P408》

 

(1)CPU是基於程序代碼和數據在時間和空間上的局部性原理預測接下來將要用來的數據,並把這個數據(含指令)裝載到高速緩存。

(2)每個CPU都有自己的高速緩存,包括指令Cache和數據Cache(其中的數據Cache一般包含多級)

(3)CPU不直接訪問主存,而是通過Cache間接的訪問內存。

(4)每次都從內存中讀取的數據不是1個字節,而是一個Cache Line(高速緩存行,可能是32字節、64字節或128字節,取決於CPU)。

8.2.2 多處理器下的讀寫問題

(1)舉例分析

  ①CPU1讀取一個字節,這使得該字節及其相鄰的若干字節被讀到CPU1的高速緩存行

  ②CPU2讀取同一個字節,同①一樣,將那串字節的數據讀到CPU2的高速緩存行

  ③設CPU1對內存的這個字節修改,被寫進CPU1的高速緩存行,但未真正寫回主存

  ④CPU2再次讀取這個字節,由於該字節己經在CPU2的高速緩存行中,因些CPU2不需再訪問內存。但CPU2將無法知道這個值已經在CPU1中得到更新的值。

(2)解決方案:

  ①當一個CPU修改了高速緩存行中的1個字節,其他CPU會收到通知,並使自己的緩存行作廢。如CPU1修改了自己的緩存行數據,CPU2中如果相關的緩存行就作廢

  ②CPU2讀取自己緩存行的該字時,發現己經作廢,則系統會調度CPU1把新值寫入主存,然后CPU2重新訪問內存來填充它的高速緩存行。可見高速緩存行提高了性能,但在多CPU的機器上同樣會損失性能。

(3)編程啟示

  ①應根據數據緩存行的大小來組織應用程序的數據,使數據與緩存行邊界對齊。目的是確保不同的CPU能夠各自訪問不同的內存地址,而這些內存地址不在同一高速緩存行中。(★獲取緩存行大小:通過GetLogicalProcessInformation,傳入SYSTEM_LOGICAL_PROCESSOR_INFORMATION結構體,從這個結構體的CACHE_DESCRIPTOR字段中的LineSize獲得。) 

  ②利用LineSize,通過__declspec(align(XXX))來對字段對齊加以控制

糟糕的數據結構設計

改進后的版本

Struct CUSTINFO{

  DWORD dwCustomerID;//只讀,經常訪問

  int  nBalanceDue;  //讀寫

  wchar_t szName[100]; //只讀,經常訪問

  FILETIME ftLstOrderDate; //讀寫

}

【說明】避免高速緩存行可能出現問題的方法

①使用局部變量或函數參數時,因為他們只讓一個線程訪問數據,就不會受其他線程影響

②設置線程親緣性,始終只讓一個CPU訪問數據。

#define CACHE_ALIGN 64 //64是從LineSize中獲取

//強迫每個結構體放在不同的緩存行中

struct __declspec(align(CACHE_ALIGN)) CUSTINFO{

  DWORD dwCustomerID;  //只讀,經常訪問

  wchar_t szName[100]; //只讀,經常訪問

  //將以下兩個字段放在不同的緩存行中

  __declspec(align(CACHE_ALIGN))

  int  nBalanceDue;  //讀寫

  FILETIME ftLstOrderDate; //讀寫

}

8.3 高級線程同步

(1)Interlocked是以原子方式修改一個值,但如果要以原子方式修改“復雜數據結構時”,需要用其他同步手段,如Critical Section、互斥鎖等。

(2)旋轉鎖的使用應謹慎,原因是浪費CPU時間比較嚴重,特別是在單CPU的機器上不應使用它。

(3)當線程無法取得對資源的訪問權或特殊事件尚未發生時,線程應進入不可調度的等待狀態,從而避免了浪費CPU的現象。

(4)volatile限定符會告訴編譯器不要對變量進行優化,而是每次讀取該變量時都從內存中獲取(有時為了提高效率,編譯器會變量放到某個寄存器,以便快速訪問。這在單線程可能沒有問題,但多線程中,這個內存中的變量可能被修改,而出現寄存器的那個變量與內存變量值的不一致,volatile會強迫總是從內存中讀取而不優化)。但注意以下區別

//變量前須加volatile

volatile int g_iX = 0;

DWORD WINAPI ThreadProc(…)

{

   //g_iX可能被其他線程修改,

   //這里強迫每次從內存讀取

   int xOrg = g_iX; //以值的形式訪問g_iX

   ……

}

//變量前無須加volatile

int g_iX = 0;

DWORD WINAPI ThreadProc(…)

{

   //以下函數是以地址方式訪

   //問g_iX,自然就要從內存中

   //中取,所以無須加volatile

   InterlockedIncrement(&g_iX);

   ……

}

(5)volatile修飾結構體時,等於結構體中所有成員都加volatile限定符。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM