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

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

2018-07-02    來源:importnew

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

譯者按

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

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

和一般的翻譯不同,我們加上了獨(dú)家的注釋。注釋要么是糾正錯(cuò)誤,要么是輔助理解,要么是補(bǔ)充擴(kuò)展;相信對大家會(huì)大有裨益。

正文翻譯:@Diting0x

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

原文地址:?Preshing

原文

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

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

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

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

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

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

鎖競爭benchmark

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

這是個(gè)泊松分布過程。如果我們知道生成單個(gè)隨機(jī)數(shù)的平均時(shí)間–在2.66GHz的四核Xeon處理器上需要6.349ns–那么我們用工作單元(work units)而不是秒來衡量時(shí)間?梢杂梦抑暗奈恼轮薪榻B的方法,How to Generate Random Timings for a Poisson Process,算出獲取與釋放鎖的時(shí)間間隔有多少個(gè)工作單元。下面代碼是C++的實(shí)現(xiàn)。我省略了一些細(xì)節(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)在假設(shè)我們運(yùn)行兩個(gè)這樣的線程,每個(gè)線程運(yùn)行在不同的CPU核心上。當(dāng)執(zhí)行任務(wù)時(shí),每個(gè)線程有一半的時(shí)間是持有鎖的,但如果其中一個(gè)線程在另一個(gè)線程持有鎖的情況下試圖獲取鎖,此線程會(huì)被強(qiáng)制等待。這就是鎖競爭。

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

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

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

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

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

鎖頻率benchmark

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

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

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

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

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

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

譯者注

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

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

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

注釋4: 本文的描述和試驗(yàn)可能讓人有點(diǎn)迷糊,這里提供一下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用來控制兩次申請鎖之間的間隔。測試的時(shí)候有三個(gè)變量:holdtime、thinktime、線程數(shù)(1個(gè)、2個(gè)、4個(gè)、直到核數(shù)的兩倍)。試試看。

標(biāo)簽: 代碼

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

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

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