中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

深入探索并發(fā)編程系列1 : 鎖不慢;鎖競爭慢

2018-07-02    來源:importnew

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

譯者按

Preshing?的博客是學習并發(fā)編程的不可多得的資料,講解比較詳細。身邊的很多朋友從中受益良多。

我們在和作者溝通后,獲得了授權(quán),著手翻譯了他的博客,刊登在這里,以饗朋友諸君。

和一般的翻譯不同,我們加上了獨家的注釋。注釋要么是糾正錯誤,要么是輔助理解,要么是補充擴展;相信對大家會大有裨益。

正文翻譯:@Diting0x

審校 && 注釋:@睡眼惺忪的小葉先森

原文地址:?Preshing

原文

鎖(也叫互斥量)在很長一段時間都被誤解了。1986年,在Usenet的有關(guān)于多線程的討論會中,Matthew Dillon說過:大多數(shù)人都對鎖有個誤解,認為鎖是慢的。25年后,這種誤解似乎在某一時間段又突然出現(xiàn)了。

在某些平臺上或者當鎖被高度競爭時,鎖確實慢。另外,當你在開發(fā)一個多線程程序時,由于鎖的引入,給性能帶來巨大的瓶頸是很常見的。但這并不意味著所有的鎖都是緩慢的。我會在這篇文章中解釋,有的時候,使用鎖的策略反而能帶來非常好的性能。

大家對鎖的誤解可能源自于某個最容易忽視的原因:不是所有的程序員都會意識到輕量級鎖和內(nèi)核鎖的區(qū)別。我會在下一篇文章中對輕量級鎖做專門介紹:總是使用輕量級鎖。在這篇文章中,假設你在Windows平臺下做C/C++開發(fā),你需要的正是一個Critical Section對象。

有時候,鎖是慢的這個結(jié)論是由benchmark支撐的。例如,這篇文章在高負載狀態(tài)下來測試鎖的性能:每個線程必須持有鎖來完成任何一項任務(高競爭),并且鎖都是在極短的時間間隔下被持有(高頻率)。這種方式似乎很完美,但在實際應用中,卻要避免這種使用鎖的方式注1;谶@種考慮,我設計了一種benchmark,同時包含對鎖使用的最壞情況和最好情況。

由于一些其它的考慮,大家可能不愿意用鎖了。存在一系列的技術(shù)被稱為無鎖編程(或者不含鎖編程注2)。無鎖編程是極具挑戰(zhàn)性的,但其本身可以在許多實際應用場景下帶來高度的性能回報。據(jù)我所知,有些程序員會花費許多天甚至幾周的時間來設計某種無鎖算法,之后再做一系列測試,但在幾個月后才發(fā)現(xiàn)隱藏的bug. 風險與回報并存對于相當一部分程序員都是有誘惑力的,這當然也包括我,在以后的幾篇文章中會提到這些。有了無鎖編程的誘惑,大家開始覺得鎖使用起來很枯燥,緩慢并且非常糟糕。

但也不能把鎖貶的一文不值。在現(xiàn)實軟件中,當大家為了保護內(nèi)存分配器的時候,鎖就是一個讓人敬仰的東西。Doug Lea的分配器是在游戲開發(fā)中非常著名的內(nèi)存分配器注3,但其只支持單線程,這時候我們就必須使用鎖機制來進行保護。在游戲運行時,經(jīng)常會碰到多個線程搶占一個內(nèi)存分配器,每秒鐘搶占次數(shù)可達到15000次左右。在加載過程中,每秒鐘會達到100000次甚至更多。雖然這并不是個大問題。但你卻可以看到,鎖能非常出色的來處理這些負載。

鎖競爭benchmark

在這次測試中,我們創(chuàng)建一個線程來生成隨機數(shù),采用傳統(tǒng)的Mersenne Twister生成器來實現(xiàn)。此線程時而獲取鎖,時而釋放鎖。獲取與釋放鎖的間隔時間是隨機的,但它都很接近我們提前決策出的平均值。舉個例子,假設我們要每秒鐘獲取鎖15000次,讓持有鎖的時間保持在總時間的50%. 下圖是部分的timeline。紅色說明鎖正在被持有,灰色說明鎖被釋放。

這是個泊松分布過程。如果我們知道生成單個隨機數(shù)的平均時間–在2.66GHz的四核Xeon處理器上需要6.349ns–那么我們用工作單元(work units)而不是秒來衡量時間?梢杂梦抑暗奈恼轮薪榻B的方法,How to Generate Random Timings for a Poisson Process,算出獲取與釋放鎖的時間間隔有多少個工作單元。下面代碼是C++的實現(xiàn)。我省略了一些細節(jié),喜歡的話,可以在?這里?下載完整的源碼

QueryPerformanceCounter(&start);
for (;;)
{
    // Do some work without holding the lock
    workunits = (int) (random.poissonInterval(averageUnlockedCount) + 0.5f);
    for (int i = 1; i < workunits; i++)
        random.integer();       // Do one work unit
    workDone += workunits;

    QueryPerformanceCounter(&end);
    elapsedTime = (end.QuadPart - start.QuadPart) * ooFreq;
    if (elapsedTime >= timeLimit)
        break;

    // Do some work while holding the lock
    EnterCriticalSection(&criticalSection);
    workunits = (int) (random.poissonInterval(averageLockedCount) + 0.5f);
    for (int i = 1; i < workunits; i++)
        random.integer();       // Do one work unit
    workDone += workunits;
    LeaveCriticalSection(&criticalSection);

    QueryPerformanceCounter(&end);
    elapsedTime = (end.QuadPart - start.QuadPart) * ooFreq;
    if (elapsedTime >= timeLimit)
        break;
}

現(xiàn)在假設我們運行兩個這樣的線程,每個線程運行在不同的CPU核心上。當執(zhí)行任務時,每個線程有一半的時間是持有鎖的,但如果其中一個線程在另一個線程持有鎖的情況下試圖獲取鎖,此線程會被強制等待。這就是鎖競爭。

在我看來,這是鎖在實際程序中應用的非常好的例子。當我們運行上述的場景時,可以發(fā)現(xiàn)每個線程會花費25%的時間在等待,75%的時間在執(zhí)行實際的任務。與單線程相比,兩個線程都獲得了1.5X的性能提升。

我在2.66GHZ的四核Xeon處理器上做過不同的測試,從一個線程到兩個線程,一直到最多四個線程的情況,每個線程都分別運行在不同的CPU核心上。同時,我還改變鎖被持有的時間,從鎖絕不被持有,到每個線程必須100%的時間持有鎖。在所有的case中,鎖頻率保持一個常數(shù)–在執(zhí)行任務過程中,線程每秒鐘獲取鎖15000次。

結(jié)果很有意思。對于短的鎖持有時間,比如持有時間占比<10%的情況, 系統(tǒng)可能達到很高的并發(fā)性。雖然不是最完美的并發(fā),但很接近。說明鎖是非常快的!

為了把結(jié)果解釋清楚一些,我用這個分析器分析了多線程游戲引擎中的內(nèi)存分配鎖.在游戲運行時,每秒鐘有15000個鎖來自三個線程,鎖的持有時間在2%左右。正好落在圖表中左側(cè)的舒適區(qū)(comfort zone).

這些結(jié)果都表明一旦鎖持有時間超過90%,就沒有必要再使用多線程了。這時,單線程會表現(xiàn)的更好。同時,最讓人吃驚的是4個線程的性能都急劇下降到60%左右。這看起來像是個異常情況,所以我又重新運行這些測試很多次了,甚至還改變了測試順序。得到的結(jié)果卻是一樣的。我對此最好的解釋就是,測試可能觸碰到了Windows分配器的盲區(qū),我沒有更進一步的去研究這個問題。

鎖頻率benchmark

一個輕量級鎖也會帶來開銷。正如我的下一篇文章中說的,對Windows Critical Section的lock/unlock成對操作會花費23.5ns(基于上述測試的CPU). 因此可以說明,每秒鐘有15000個鎖已經(jīng)足夠少了,鎖的開銷并不會在很大程度上影響整個結(jié)果。但如果我們提高鎖頻率,又會發(fā)生什么呢?

算法中,嚴格控制鎖與鎖之間執(zhí)行的任務數(shù),因此我做了一系列新的測試:鎖與鎖的間隔時間從10ns到最高31ns(相對應每秒鐘大約32000次鎖).每次測試都使用兩個線程。

正如你想的那樣,鎖頻率很高的話,鎖本身的開銷就已經(jīng)高于所執(zhí)行的任務本身了. 在網(wǎng)上找到的一些benchmarks,包括前面提到的那個分析器, 都落在圖表中的右下角。在這些高頻率下,和一些CPU指令的規(guī)模一樣,鎖的間隔比較小。好消息是,當鎖與鎖之間的任務比較簡單的時候,無鎖編程更可行。

與此同時,結(jié)果表明當鎖頻率達到每秒鐘32000次時(鎖間隔是3.1us)也是可以接受的。在游戲開發(fā)中,內(nèi)存分配器就可能會在加載過程中達到這個頻率。如果鎖間隔比較短暫,你仍然可以得到1.5X的并發(fā)度。

我們已經(jīng)了解了一系列鎖性能的例子:有性能表現(xiàn)的很好的時候,也有性能慢的跟爬蟲似的時候。我已經(jīng)證明了游戲引擎中的內(nèi)存分配器一直都能保持非常好的性能。把這個例子運用到實際場景中,不能說鎖是慢的。不得不承認,鎖很容易被濫用,但你不必太害怕–只要經(jīng)過仔細的分析,任何情況下都能找出導致性能瓶頸的因素。當你正在考慮鎖有多可靠,并去理解鎖的相關(guān)優(yōu)化方法時(相比無鎖編程),鎖有時候表現(xiàn)的真的非常出色。

寫這篇文章的目的是為了讓鎖得到應有的尊敬–歡迎批評指正。鎖在工業(yè)應用程序中有廣泛的應用,至于鎖的性能,并不總是能達到一個很好的均衡。如果你在自己的經(jīng)驗中發(fā)現(xiàn)類似這樣的例子,非常樂意看到你的評論注4。

譯者注

注釋1:一種避免或者降低鎖沖突的科學思想是partition,避免資源集中。例如,對于hashtable,可以由之前的一個hashtable對應一把鎖,改為每個bucket配置一把鎖,這樣沖突將大大降低。再例如,計數(shù)程序,如果大家都對同一個全局變量進行讀寫而加一把鎖,那么沖突嚴重,可以適當?shù)倪x擇多個計數(shù)器,不同的線程累加對應的計數(shù)器,一個線程負責將這些計數(shù)器的值求和。等等等等。

注釋2: 這里的無鎖編程,原文為lock free。不含鎖編程,原文為lockless。但是需要注意的是,lock free并不是無鎖的意思,它的本質(zhì)是說一組線程,總有(至少)一個線程能make progress,和有沒有鎖沒有本質(zhì)聯(lián)系。lock free目前一般都翻譯為無鎖(有些地方也翻譯為鎖無關(guān)),因此本文也采用這種譯法,但是讀者需要特別注意。另外lockless就是真正的無鎖、不包含鎖的編程。

注釋3: Doug Lea是并發(fā)編程的大牛,《Java并發(fā)編程實戰(zhàn)》的作者之一,非常樂意分享。他寫的這個分配器非常出名,glibc所采用的內(nèi)存分配器實現(xiàn)就是基于他設計的算法。

注釋4: 本文的描述和試驗可能讓人有點迷糊,這里提供一下Paul E. McKenney大叔在他的著作《Is Parallel Programming Hard, And, If So, What Can You Do About It?》中第4章中的例子來解釋,讓讀者更好的理解:

pthread_rwlock_t rwl = PTHREAD_RWLOCK_INITIALIZER;
int holdtime = 0;
int thinktime = 0;
long long *readcounts;
int nreadersrunning = 0;
#define GOFLAG_INIT 0
#define GOFLAG_RUN  1
#define GOFLAG_STOP 2
char goflag = GOFLAG_INIT;
void *reader(void *arg)
{
   int i;
   long long loopcnt = 0;
   long me = (long)arg;
   __sync_fetch_and_add(&nreadersrunning, 1);
   while (ACCESS_ONCE(goflag) == GOFLAG_INIT) {
     continue;
   }
   while (ACCESS_ONCE(goflag) == GOFLAG_RUN) {
     if (pthread_rwlock_rdlock(&rwl) != 0) {
       perror("pthread_rwlock_rdlock");
       exit(-1);
     }
     for(i=1;i<holdtime;i++){
       barrier();
     }
     if (pthread_rwlock_unlock(&rwl) != 0) {
       perror("pthread_rwlock_unlock");
       exit(-1);
     }
     for (i=1;i<thinktime;i++) {
       barrier();
     }
     loopcnt++;
   }
   readcounts[me] = loopcnt;
   return NULL;

其中16-18行等待測試開始的信號;19行開始測試;holdtime控制臨界區(qū)的長短,thinktime用來控制兩次申請鎖之間的間隔。測試的時候有三個變量:holdtime、thinktime、線程數(shù)(1個、2個、4個、直到核數(shù)的兩倍)。試試看。

標簽: 代碼

版權(quán)申明:本站文章部分自網(wǎng)絡,如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:kafka源碼分析3 : Producer

下一篇:Service Mesh 及其主流開源實現(xiàn)解析