運行時: 使進程和線程同步

來源:互聯網
上載者:User
內容:


進程和線程同步
計時器迴圈
互斥鎖
結束語
再論管道
參考資料
關於作者
對本文的評價
相關內容:
Linux、Windows 2000 和 Windows XP 中的管道
塊記憶體複製,第一部分
塊記憶體複製,第二部分
訂閱:
developerWorks 時事通訊

Linux 和 Windows 上的高效能編程技術

Edward G. Bradford 博士
進階程式員, IBM
2001 年 10 月 19 日

在 Ed 以前的文章中談到了管道,一種 Windows 和 Linux 上處理序間通訊的形式。本文中,他探討了進程間同步原語(作為控制兩個進程訪問同一資源的方法)。象往常一樣,Ed 的目的是推薦幾種方法來最佳化編程實踐,並且示範了那些編程實踐的效能。單擊本文頂部或底部的討論進入論壇,與作者以及其他讀者分享您對本文的看法。

在開始之前,讓我們先來回顧幾個用於各種作業系統的術語:當不區分 Windows 2000 和 Windows XP 時,使用“Windows”。當要進行區分時,使用“Windows 2000”或者“Windows XP”。

進程和線程同步
本文中,我要集中探討實現進程間和線程間同步的許多方法。通過同步,可以進行受控訪問。例如,如果兩個進程(或者線程)希望更新同一個共用記憶體計數器,那麼這個計數器必須由這兩個進程分別作原子更新。為實現這一點,進程必須控制計數器足夠長的時間以便從記憶體中讀取計數器,對它進行增量操作,然後將它返回記憶體。在大多數電腦上,這個操作由多條機器指令構成。在多指令更新過程中,為使作業系統避免發生意外的上下文環境切換,可以使用進程同步原語。

進程同步需要相互協作。每個進程必須同意遵守同步的規則。一旦達成協議,則有許多機制可用於同步。一些機制只適合於線程,一些機制適合於進程,還有一些甚至運用於網路上進行進程或線程間的同步。本文將研究專為同步而設計的電腦內的原語:

  • 訊號量(Semaphores)
  • 互斥鎖(Mutexes)
  • 臨界段(Critical sections)

讓我們從訊號量開始。它可以由不同的線程或者不同的進程使用。訊號量作為計數器來實現。為獲得獨佔控制,線程必須“獲得”訊號量。一個“獲得”轉化成從訊號量值中減 1。如果訊號量的當前值為 0,那麼進程就阻塞,直到該值減 1 後的結果大於或等於 0 為止。作業系統保證在其它線程或者進程試圖進行相同的減法和 0 測試操作中,這個操作是一個原子操作。因此,為擷取訊號量,進程就嘗試減 1 操作。如果這個操作所得到的結果是個負值,進程就阻塞。

在 Windows 上,訊號量操作是 ReleaseSemaphore()WaitForSingleObject() 。ReleaseSemaphore() 對應於給訊號量的值加 1。 WaitForSingleObject() 對應於給訊號量的值減 1。

在 Linux 上,有兩類訊號量。第一類是由 semget/semop/semctl API 定義的訊號量的 SVR4(System V Release 4)版本。第二類是由 sem_init/sem_wait/sem_post/interfaces 定義的 POSIX 介面。 它們具有相同的功能,但介面不同。我寫了一個程式,用來測試這些介面並對它們計時。這是一個單線程程式。(實際上,存在兩個線程,但同步原語都以單線程方式執行。)

在我們查看代碼之前,先扯開來談些別的。編寫計時迴圈涉及到猜測適當最大值的迴圈計數器來產生足夠多的執行次數。在所討論介面的基本計時已知之前,通過計時迴圈挑選合適的迴圈次數是個純粹猜測的工作。通過在一段固定的時間內運行迴圈,本文程式中消除了這個問題。不是計算執行一百萬次操作需要的時間,而是在一段固定的時間內,對執行的次數進行計數。 我選擇 2 秒作為預設值;本文中的所有計時都指兩秒的執行循環。

計時器迴圈
計時器迴圈在一段固定的時間後簡單地將單個全域標誌的值更改為 0。當全域標誌的值為 1 時,實際的執行迴圈對執行的次數進行計數。當全域標誌的值變為 0 時,迴圈終止並且提交計時報告。 為實現計時器, starttimedtest() 常式建立了一個線程,它只負責休眠幾個納秒後,將標誌值更改為 0,並退出。計時測試公用程式顯示如下。

計時測試公用程式

        volatile int run_count = 0;    int startTimedTest(volatile int *flg)    {        static int first = 1;        if(first) {    #ifdef _WIN32            InitializeCriticalSection(&lock_run_count);    #else            (void)pthread_mutex_init(&lock_run_count,NULL);    #endif            first = 0;        }        run_count = 0;    #ifdef _WIN32        th1 = CreateThread(NULL, 4096,timerloop,(char *)flg,NULL,&timerId);        if(th1 == NULL) {            printf("CreateThread FAILED: err=%d/n",errno);            return 1;        }    #else    #   define DEC (void *(*)(void *))        if(pthread_create(&tA,NULL,DEC timerloop,(void *)&timedtestflag)) {            printf("pthread_create FAILED: err=%d/n", errno);            return 1;        }    #endif        while(run_count != 1)            YIELD;        return 0;    }    unsigned long WINDEC timerloop(void *v)    {        int *flg = (int *)v;        LOCK(&lock_run_count);        run_count++;        UNLOCK(&lock_run_count);        *flg = 1;        SLEEP(nseconds);        *flg = 0;        return 0;    }    void endTimedTest()    {    #ifdef _WIN32        (void)WaitForSingleObject(&th1, INFINITE);    #else        if(pthread_join(tA, (void **)&threadreturn)) {            printf("pthread_join FAILED: err=%d/n",errno);            return;        }    #endif    }    

注意 starttimedtest()volatile int *flg 自變數。為瞭解釋 C 或者 C++ 中的關鍵字 volatile 或者 const ,只需由右至左閱讀聲明。傳遞了一個 volatile 的整數指標作為 starttimedtest() 其參數。“Volatile”指的是不允許編譯器最佳化對 *flg 運算式指定記憶體的間接引用。

使用計時公用程式以及 timedtest 介面,Windows 訊號量計時迴圈程式顯示如下。

計時 Windows 訊號量操作

        semaA = CreateSemaphore(NULL, 0, 1, "semaABC");    if(semaA == NULL) {        printf("CreateSemaphore /"semaABC/" failed ERROR=%d/n", GetLastError());        return 1;    }    count = 0;    timedtestflag  = 0;    if(startTimedTest(&timedtestflag))        return 1;    tstart();    while(timedtestflag) {        count++;        //        // Increment        //        if(!ReleaseSemaphore(semaA,1,0)) {            printf("ReleaseSema failed: error=%d/n",GetLastError());            return 1;        }        //        // Decrement        //        if(WaitForSingleObject(semaA, INFINITE) == WAIT_FAILED) {            printf("Wait in ALREADY_EXISTS child failed err=%d/n",                GetLastError());            return 1;        }    }    tend();    endTimedTest();    t = tval();    

計時測試公用程式為指定時間內運行所有我們的測試提供了一個簡單的架構。只需要啟動計時器,對執行的次數進行計數,直到標誌值回到 0 為止。這些公用程式本可以與作為背景工作執行緒的附加線程一起編寫。那樣有利於向 starttimedtest() 常式(真正用來計時的函數)提供一個自變數。我選擇了在另一個線程中執行計時器,在只用於測試代碼可視性的主線程中加入待測代碼。 訊號量的文檔存放在 Windows Platform SDK(訊號量對象)和 Linux 協助頁(輸入 man semop 可找到有關 System V 訊號量的協助頁面,輸入 man sem_init 可找到有關 POSIX 訊號量的頁面)中。

互斥鎖
互斥鎖是那些值只能為 0 或 1 的訊號量。一個互斥鎖可以作為一個入口,每次只讓一個線程或進程訪問一個資源。互斥鎖表示 互相 排斥 線程的訪問;在給定的時間內,只有一個線程可以“擁有”一個互斥鎖。互斥鎖與訊號量很相似並且它們可以在支援訊號量的同一作業系統代碼下實現。事實上,從下面的計時評測中可以看出,可能 Windows 正好是這樣做的。UNIX 和 Linux 中的訊號量先於 POSIX 線程支援的互斥鎖出現。

互斥鎖的文檔存放於 Windows Platform SDK(互斥鎖對象)和 Linux 協助頁( man pthread_mutex_lock )中。

最後,Windows 有一種稱為臨界段(Critical Section)的方法。 臨界段為相互排斥提供了最低開銷機制,但是只能由單個進程中的線程使用。它們的行為與互斥鎖很相似;但開銷卻相當少。它們的文檔存放於 Platform SDK(臨界段對象)中。

我寫了一個單個程式,用來練習加鎖/解鎖介面。它基於前面提到的計時測試公用程式。在 Windows 2000 和 Windows XP 上運行 sync6.cpp 的結果 1 所示。


圖 2 顯示了在 Red Hat 7.1 Linux(其核心版本是 Linux 2.4.2)上啟動並執行結果。


圖 1 和圖 2 出自表 1 和表 2,兩表明確顯示了 sync6.cpp 程式評測的時間。

介面 Win2K WinXP
Mutex 2.629 2.191
Sema 2.555 2.149
CriticalSection 0.046 0.129
介面

Linux 2.4.2

SVR5_Semaphores 1.828
POSIX_Semaphores 0.487
pthread_mutex 0.262

表 1 Windows 同步原語(usec/call-pair)

表 2 Linux 同步原語(usec/call-pair)

在第一張圖上可以看到,Windows XP 訊號量和互斥鎖的執行次數改進為只有 Windows 2000 的 83%。CriticalSection API 看起來在執行時間上提高了 280%。CriticalSection API 應該取決於硬體記憶體互鎖指令,所以為什麼 Windows XP 中的 CriticalSection API 比 Windows 2000 中的慢得多,這一點不是很清楚。

Linux 介面顯示了傳統 SVR5 訊號量是最慢的執行者,而 pthread 互斥鎖是最快的執行者。儘管 Windows XP CriticalSection API 的速度降低了下來,但它們仍然比 Linux pthread 互斥鎖快 2 倍。

也可能使用其它機制來處理進程或者線程同步。例如,塊通訊通道可以用於同步。塊通道包括管道、通訊端、串列線和紅外通道。在這個星期的測試中,最長的時間是 Windows 2000 互斥鎖,為 2.62 微秒/調用對。如果使用 Windows 上的具名管道,那麼我們可以希望得到的最好的一點是移動最少資料量所涉及的開銷。那將是一個單位元組。在我 以前關於管道的文章中公布的試算表上可以得出,使用一個 1 位元組大小的塊產生的結果是每秒鐘移動 144,000 個位元組。那相當於每秒寫/讀 144,000 次,或者 6.944 微秒/調用對。 這要比這周測試中所記錄的對 Windows 上任何原語的測試結果都要糟糕。除非有其它原因,否則將管道用於同步機制是個很差的效能選擇。

Linux 管道的速度比 Windows 管道的速度快。Linux 上移動一個一位元組大小的塊,其結果是每秒 500,000 位元組或者每秒 500,000 個調用對或者每個調用對 2.0 微秒。管道幾乎與 SVR4 訊號量一樣好(1.828 微秒),但是比 POSIX 訊號量(0.487 微秒)或者 pthread 互斥對象(0.262 微秒)差了許多。除非要考慮其它事項,否則管道對於 Linux 上的進程同步也不是一個好的選擇。

結束語
我寫了一個程式 sync6.cpp 來示範 Windows 和 Linux 上各種進程和線程同步原語的用途並評測了它們的效能。 我們發現其中最快的原語是 Windows 臨界段。我們還看到 Windows 2000 與 Windows XP 的訊號量效能一起得到了改進,但是 Windows XP 的 CriticalSection API 減慢了。

再論管道
在我 以前關於管道的文章中有許多假設。許多讀者對此提供了協助,發表了自己的評論。其中提出的最值得注意的問題,也是我的錯誤處,即,使用通過管道的任何記憶體。 當我開始評測代碼路徑開銷以及公布每秒鐘的位元組數時,我的想法就偏了。那的確是個錯誤。

因此,作為本文的附錄,我公布了上月 pipespeed3t.cpp 程式的修正版。讀者提出的所有問題都在這個版本中解決了。圖 3 顯示了在 Linux、Windows 2000 和 Window XP 上運行 pipespeed3t 產生的結果。對於每個作業系統,一種運行不通過訪問記憶體來完成,另一種運行涉及到讀和寫緩衝區。這種記憶體訪問由對每次寫入不同資料的每個寫緩衝區進行 memset() 操作以及對每個讀緩衝區進行 memcpy 操作組成。 對結果繪圖,其中,紅色代表 Windows XP,綠色代表 Windows 2000,藍色代表 Linux。 粗線代表訪問記憶體評測,細線代表未訪問記憶體評測。


可以看出,所有曲線的形狀幾乎相同,相關的效能未改變。對於 Windows 2000 和 Linux,訪問記憶體的管道的傳輸速度下降到不訪問記憶體的約 60%。對於 Windows XP,是否訪問記憶體看起來沒有發生很大的區別。

曾有少數關於比較“蘋果和桔子”(未具名管道與具名管道)的評論。表面上看,它們似乎完全不同。以前我曾比較了這兩種管道,但是忽略了在本專欄文章中將它指出。因此,為滿足讀者的要求以及為了證實我的記憶力,我添加了幾個選項,在 Linux( mknod() API)上使用具名管道,在 Windows 上使用未具名管道( CreatePipe() API)。

因此,我比較了 Linux 未具名管道和 Linux 具名管道。 這兩種介面的特徵是: pipe() 系統調用用於未具名管道, mknod() 系統調用用於具名管道。 圖 4 顯示了結果。這兩種方法看起來差別不大。


在 Windows 2000 上,運行了對緩衝區空間大小的搜尋並將之與 CreateNamedPipe() 的結果相比較。圖 5 顯示了比較結果。圖中帶“X”的曲線是 CreateNamedPipe() 的結果。可以看到通過在 CreatePipe() API 內設定緩衝區的大小可獲得管道速度的一些改進。


在 Windows XP 上運行了相同的測試,結果顯示在圖 6 中。這裡的黑色菱形塊顯示了 CreateNamedPipe() 值。Windows 2000 在管道中傳輸資料要比 Windows XP 快約三倍。


從這些圖表中我們看到,Windows 2000 和 Windows XP 上的具名管道可能基於相同的底層技術。Windows 2000 在 CreatePipe() API 呼叫上顯示了一些改進,即仔細地進行緩衝區大小的選擇。Pipespeed3t 提供了這些圖表的評測程式。原始碼可在 參考資料一節上找到。

參考資料

  • 您可以參閱本文在 developerWorks 全球網站上的 英文原文.

  • 單擊本文頂部或底部的 討論,參與關於本文的 論壇。
  • 閱讀我以前 developerWorks中關於 管道的文章。
  • 本文中用到的代碼檔案:
    • sync6.cpp
    • sync6.xls
    • pipespeed3t.cpp

  • 閱讀 developerWorks上 Ed 的其它“運行時”專欄文章:
    • 介紹性專欄
    • Block memory copy
    • Block memory copy, Part 2

  • 閱讀 developerWorks上的相關文章:
    • Operating system flexibility
    • Linux, the server operating system

  • 請瀏覽 developerWorks上 更多 Linux 參考資料
  • 請瀏覽 developerWorks上 更多開放原始碼參考資料。
關於作者
Edward Bradford 博士現在為 IBM Software Group 管理 Microsoft Premier Support,並且每周還為 “Linux 和 Windows 2000 軟體開發人員”撰寫新聞簡報。可以通過 egb@us.ibm.com與他聯絡。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.