亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  C++ 并行編程中的“鎖”難題

C++ 并行編程中的“鎖”難題

時間:2024-02-11
轉自:Guancheng (G.C.)

在并行程序中,鎖的使用會主要會引發兩類難題:一類是諸如死鎖、活鎖等引起的多線程Bug;另一類是由鎖競爭引起的性能瓶頸。本文將介紹并行編程中因為鎖引發的這兩類難題及其解決方案。


1、用鎖來防止數據競跑

在進行并行編程時,我們常常需要使用鎖來保護共享變量,以防止多個線程同時對該變量進行更新時產生數據競跑(Data Race)。所謂數據競跑,是指當兩個(或多個)線程同時對某個共享變量進行操作,且這些操作中至少有一個是寫操作時所造成的程序錯誤。例1中的兩個線程可能同時執行“counter++”從而產生數據競跑,造成counter最終值為1(而不是正確值2)。

例1:

#include <pthread.h>
int counter = 0;
void *func(void *params)
{
    counter++; //數據競跑
}
void main()
{
    pthread_t thread1, thread2;
    pthread_create(&thread1, 0, func, 0);
    pthread_create(&thread2, 0, func, 0);
    pthread_join(thread1, 0 );
    pthread_join(thread2, 0 );
}

這是因為counter++本身是由三條匯編指令構成的(從主存中將counter的值讀到寄存器中;對寄存器進行加1操作;將寄存器中的新值寫回主存),所以例1中的兩個線程可能按如下交錯順序執行,導致counter的最終值為1:

例2:

load [%counter], rax; // 線程1從counter讀取0到寄存器rax
add rax, 1// 線程1對寄存器rax進行加1
load [%counter], rbx; // 線程2從counter讀取0到寄存器rbx
store rax [%counter]; // 線程1把1寫入counter的主存地址
add rbx, 1// 線程2對寄存器rbx進行加1
store rbx, [%counter]; // 線程2把1寫入counter的主存地址

為了防止例1中的數據競跑現象,我們可以使用鎖來保證每個線程對counter++操作的獨占訪問(即保證該操作是原子的)。在例3的程序中,我們使用mutex鎖將counter++操作放入臨界區中,這樣同一時刻只有獲取鎖的線程能訪問該臨界區,保證了counter++的原子性:即只有在線程1執行完counter++的三條指令之后線程2才能執行counter++操作,保證了counter的最終值必定為2。

例3:

#include <pthread.h>
int counter = 0;
pthread_mutex_t mutex;
void *func(void *params)
{
    pthread_mutex_lock(&mutex);
    counter++; //處于臨界區,不會產生數據競跑
    pthread_mutex_unlock(&mutex);
}
void main()
{
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex);
    pthread_create(&thread1, 0, func, 0);
    pthread_create(&thread2, 0, func, 0);
    pthread_join(thread1, 0 );
    pthread_join(thread2, 0 );
    pthread_mutex_destroy(&mutex);
}

2. 死鎖和活鎖

然而,鎖的使用非常容易導致多線程Bug,最常見的莫過于死鎖和活鎖。從原理上講,死鎖的產生是由于兩個(或多個)線程在試圖獲取正被其他線程占有的資源時造成的線程停滯。在下例中,假設線程1在獲取mutex_a鎖之后正在嘗試獲取mutex_b鎖,而線程2此時已經獲取了mutex_b鎖并正在嘗試獲取mutex_a鎖,兩個線程就會因為獲取不到自己想要的資源、且自己正占有著對方想要的資源而停滯,從而產生死鎖。

例4:

// 線程 1                     
void func1()                    
{                       
    LOCK(&mutex_a);                      
    LOCK(&mutex_b);//線程1停滯在此         
    counter++;                       
    UNLOCK(&mutex_b);                     
    UNLOCK(&mutex_a);                    
}                       
 
// 線程 2
void func2()
{
    LOCK(&mutex_b);
    LOCK(&mutex_a);//線程2停滯在此
    counter++;
    UNLOCK(&mutex_a);
    UNLOCK(&mutex_b);
}

例4中的死鎖其實是最簡單的情形,在實際的程序中,死鎖往往發生在復雜的函數調用過程中。在下面這個例子中,線程1在func1()中獲取了mutex_a鎖,之后調用func_call1()并在其函數體中嘗試獲取mutex_b鎖;與此同時線程2在func2()中獲取了mutex_b鎖之后再在func_call2()中嘗試獲取mutex_a鎖從而造成死鎖。可以想象,隨著程序復雜度的增加,想要正確的檢測出死鎖會變得越來越困難。

例5:

// 線程 1                     
void func1()                    
{                       
LOCK(&mutex_a);                      
...                     
func_call1();                
UNLOCK(&mutex_a);                
}                       
 
func_call1()                    
{                       
   LOCK(&mutex_b);               
   ...                       
   UNLOCK(&mutex_b);                 
   ...                       
}                       
 
// 線程 2
void func2()
{
    LOCK(&mutex_b);
    ...
    func_call2()
    UNLOCK(&mutex_b);
}
 
func_call2()
{
    LOCK(&mutex_a);
    ...
    UNLOCK(&mutex_b);
    ...
}

其實避免死鎖的方法非常簡單,其基本原則就是保證各個線程加鎖操作的執行順序是全局一致的。例如,如果上例中的線程1和線程2都是先對mutex_a加鎖再對mutex_b進行加鎖就不會產生死鎖了。在實際的軟件開發中,除了嚴格遵守相同加鎖順序的原則防止死鎖之外,我們還可以使用RAII(Resource Acquisition Is Initialization,即“資源獲取即初始化”)的手段來封裝加鎖解鎖操作,從而幫助減少死鎖的發生[1]。

除死鎖外,多個線程的加鎖、解鎖操作還可能造成活鎖。在下例中,程序員為了防止死鎖的產生而做了如下處理:當線程1在獲取了mutex_a鎖之后再嘗試獲取mutex_b時,線程1通過調用一個非阻塞的加鎖操作(類似pthread_mutex_trylock)來嘗試進行獲得mutex_b:如果線程1成功獲得mutex_b,則trylock()加鎖成功并返回true,如果失敗則返回false。線程2也使用了類似的方法來保證不會出現死鎖。不幸的是,這種方法雖然防止了死鎖的產生,卻可能造成活鎖。

例如,在線程1獲得mutex_a鎖之后嘗試獲取mutex_b失敗,則線程1會釋放mutex_a并進入下一次while循環;如果此時線程2在線程1進行TRYLOCK(&mutex_b)的同時執行TRYLOCK(&mutex_a),那么線程2也會獲取mutex_a失敗,并接著釋放mutex_b及進入下一次while循環;如此反復,兩個線程都可能在較長時間內不停的進行“獲得一把鎖、嘗試獲取另一把鎖失敗、再解鎖之前已獲得的鎖“的循環,從而產生活鎖現象。當然,在實際情況中,因為多個線程之間調度的不確定性,最終必定會有一個線程能同時獲得兩個鎖,從而結束活鎖。盡管如此,活鎖現象確實會產生不必要的性能延遲,所以需要大家格外注意。

例6:

// 線程 1                     
void func1()                    
{                       
    int done = 0;                   
    while(!done) {               
        LOCK(&mutex_a);                      
        if (TRYLOCK(&mutex_b)) {               
            counter++;                   
            UNLOCK(&mutex_b);                        
            UNLOCK(&mutex_a);                        
            done = 1;                        
        }                          
        else {                     
            UNLOCK(&mutex_a);                   
        }                          
    }                        
}                       
 
// 線程 2
void func2()
{
    int done = 0;
    while(!done) {
        LOCK(&mutex_b);
        if (TRYLOCK(&mutex_a)) {
            counter++;
            UNLOCK(&mutex_a);
            UNLOCK(&mutex_b);
            done = 1; 
        }
        else {
            UNLOCK(&mutex_b);
        }
    }
}

3. 鎖競爭性能瓶頸

在多線程程序中鎖競爭是最主要的性能瓶頸之一。在前面我們也提到過,通過使用鎖來保護共享變量能防止數據競跑,保證同一時刻只能有一個線程訪問該臨界區。但是我們也注意到,正是因為鎖造成的對臨界區的串行執行導致了并行程序的性能瓶頸。

3.1阿姆達爾法則(Amdahl’s Law)

在介紹鎖競爭引起的性能瓶頸之前,讓我們先來了解一下阿姆達爾法則。我們知道,一個并行程序是由兩部分組成的:串行執行的部分和可以并行執行的部分。假設串行部分的執行時間為S,可并行執行部分的執行時間為P,則整個并行程序使用單線程(單核)串行執行的時間為S+P。阿姆達爾法則規定,可并行執行部分的執行時間與線程數目成反比:即如果有N個線程(N核CPU)并行執行這個可并行的部分,則該部分的執行時間為P/N。由此我們可以得到并行程序總體執行時間的公式:

總體執行時間 T = S + P/N

根據這個公式,我們可以得到一些非常有意思的結論。例如,如果一個程序全部代碼都可以被并行執行,那么它的加速比會非常好,即隨著線程數(CPU核數)的增多該程序的加速比會線性遞增。換句話說,如果單線程執行該程序需要16秒鐘,用16個線程執行該程序就只需要1秒鐘。

然而,如果這個程序只有80%的代碼可以被并行執行,它的加速比卻會急劇下降。根據阿姆達爾法則,如果用16個線程并行執行次程序可并行的部分,該程序的總體執行時間T = S + P/N = (160.2) + (160.8)/16 = 4秒,這比完全并行化的情況(只需1秒)足足慢了4倍!實際上,如果該程序只有50%的代碼可以被并行執行,在使用16個線程時該程序的執行時間仍然需要8.5秒!

從阿姆達爾法則我們可以看到,并行程序的性能很大程度上被只能串行執行的部分給限制住了,而由鎖競爭引起的串行執行正是造成串行性能瓶頸的主要原因之一。

3.2鎖競爭的常用解決辦法

3.2.1 避免使用鎖

為了提高程序的并行性,最好的辦法自然是不使用鎖。從設計角度上來講,鎖的使用無非是為了保護共享資源。如果我們可以避免使用共享資源的話那自然就避免了鎖競爭造成的性能損失。幸運的是,在很多情況下我們都可以通過資源復制的方法讓每個線程都擁有一份該資源的副本,從而避免資源的共享。如果有需要的話,我們也可以讓每個線程先訪問自己的資源副本,只在程序的后講各個線程的資源副本合并成一個共享資源。例如,如果我們需要在多線程程序中使用計數器,那么我們可以讓每個線程先維護一個自己的計數器,只在程序的最后將各個計數器兩兩歸并(類比二叉樹),從而最大程度提高并行度,減少鎖競爭。

3.2.2 使用讀寫鎖

如果對共享資源的訪問多數為讀操作,少數為寫操作,而且寫操作的時間非常短,我們就可以考慮使用讀寫鎖來減少鎖競爭。讀寫鎖的基本原則是同一時刻多個讀線程可以同時擁有讀者鎖并進行讀操作;另一方面,同一時刻只有一個寫進程可以擁有寫者鎖并進行寫操作。讀者鎖和寫者鎖各自維護一份等待隊列。當擁有寫者鎖的寫進程釋放寫者鎖時,所有正處于讀者鎖等待隊列里的讀線程全部被喚醒并被授予讀者鎖以進行讀操作;當這些讀線程完成讀操作并釋放讀者鎖時,寫者鎖中的第一個寫進程被喚醒并被授予寫者鎖以進行寫操作,如此反復。

換句話說,多個讀線程和一個寫線程將交替擁有讀寫鎖以完成相應操作。這里需要額外補充的一點是鎖的公平調度問題。例如,如果在寫者鎖等待隊列中有一個或多個寫線程正在等待獲得寫者鎖時,新加入的讀線程會被放入讀者鎖的等待隊列。這是因為,盡管這個新加入的讀線程能與正在進行讀操作的那些讀線程并發讀取共享資源,但是也不能賦予他們讀權限,這樣就防止了寫線程被新到來的讀線程無休止的阻塞。

需要注意的是,并不是所有的場合讀寫鎖都具備更好的性能,大家應該根據Profling的測試結果來判斷使用讀寫鎖是否能真的提高性能,特別是要注意寫操作雖然很少但很耗時的情況。

3.2.3 保護數據而不是操作

在實際程序中,有不少程序員在使用鎖時圖方便而把一些不必要的操作放在臨界區中。例如,如果需要對一個共享數據結構進行刪除和銷毀操作,我們只需要把刪除操作放在臨界區中即可,資源銷毀操作完全可以在臨界區之外單獨進行,以此增加并行度。正是因為臨界區的執行時間大大影響了并行程序的整體性能,我們必須盡量少在臨界區中做耗時的操作,例如函數調用,數據查詢,I/O操作等。簡而言之,我們需要保護的只是那些共享資源,而不是對這些共享資源的操作,盡可能的把對共享資源的操作放到臨界區之外執行有助于減少鎖競爭帶來的性能損失。

3.2.4 盡量使用輕量級的原子操作

在例3中,我們使用了mutex鎖來保護counter++操作。實際上,counter++操作完全可以使用更輕量級的原子操作來實現,根本不需要使用mutex鎖這樣相對較昂貴的機制來實現。在今年程序員第四期的《volatile與多線程的那些事兒》中我們就有對Java和C/C++中的原子操作做過相應的介紹。

3.2.5 粗粒度鎖與細粒度鎖

為了減少串行部分的執行時間,我們可以通過把單個鎖拆成多個鎖的辦法來較小臨界區的執行時間,從而降低鎖競爭的性能損耗,即把“粗粒度鎖”轉換成“細粒度鎖”。但是,細粒度鎖并不一定更好。這是因為粗粒度鎖編程簡單,不易出現死鎖等Bug,而細粒度鎖編程復雜,容易出錯;而且鎖的使用是有開銷的(例如一個加鎖操作一般需要100個CPU時鐘周期),使用多個細粒度的鎖無疑會增加加鎖解鎖操作的開銷。在實際編程中,我們往往需要從編程復雜度、性能等多個方面來權衡自己的設計方案。

事實上,在計算機系統設計領域,沒有哪種設計是沒有缺點的,只有仔細權衡不同方案的利弊才能得到最適合自己當前需求的解決辦法。例如,Linux內核在初期使用了Big Kernel Lock(粗粒度鎖)來實現并行化。從性能上來講,使用一個大鎖把所有操作都保護起來無疑帶來了很大的性能損失,但是它卻極大的簡化了并行整個內核的難度。當然,隨著Linux內核的發展,Big Kernel Lock已經逐漸消失并被細粒度鎖而取代,以取得更好的性能。

3.2.6 使用無鎖算法、數據結構

首先要強調的是,筆者并不推薦大家自己去實現無鎖算法。為什么別去造無鎖算法的輪子呢?因為高性能無鎖算法的正確實現實在是太難了。有多難呢? Doug Lea 提到 java.util.concurrent 庫中一個Non Blocking的算法的實現大概需要1個人年,總共約500行代碼。事實上,我推薦大家直接去使用一些并行庫中已經實現好了的無鎖算法、無鎖數據結構,以提高并行程序的性能。典型的無鎖算法的庫有java.util.concurrent,Intel TBB等,它們都提供了諸如Non-blocking concurrent queue之類的數據結構以供使用。

往期推薦


關于堆棧的講解(我見過的最經典的)

【程序員必讀】經驗:編程的智慧

2021最穩C/C++學習路線

怎么寫代碼能讓 CPU 執行更快?

寫出高效代碼的12條建議

亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
亚洲天堂网站在线观看视频| 亚洲国产免费看| 欧美日韩激情小视频| 国产精品白丝jk黑袜喷水| 欧美第一黄色网| 在线成人激情黄色| 欧美国产精品| 国产一区二区三区免费观看| 韩国一区二区在线观看| 亚洲午夜精品网| 欧美视频不卡| 亚洲性视频网址| 国产精品国产三级国产aⅴ入口 | 午夜精品美女自拍福到在线| 亚洲欧美影音先锋| 国产免费一区二区三区香蕉精| 亚洲第一久久影院| 欧美刺激午夜性久久久久久久| 一本大道久久a久久综合婷婷| 欧美精品日韩| 亚洲私拍自拍| 黄色国产精品一区二区三区| 国产精品www网站| 亚洲午夜一区| 韩国女主播一区二区三区| 蜜臀91精品一区二区三区| 亚洲肉体裸体xxxx137| 欧美视频二区| 久久中文欧美| 亚洲欧美在线aaa| 久久av二区| 揄拍成人国产精品视频| 免费在线一区二区| 久久99伊人| 午夜激情一区| 在线一区二区三区做爰视频网站| 黄色在线成人| 国产精品欧美一区二区三区奶水| 久久夜色精品| 久久精品日产第一区二区三区| 亚洲乱码一区二区| 一区免费观看| 国产一区视频网站| 欧美成人免费视频| 久久久久久黄| 久久久久久久久久久久久女国产乱 | 欧美色大人视频| 欧美va亚洲va香蕉在线| 欧美中文字幕在线| 久久精品电影| 欧美国产精品v| 欧美aa在线视频| 欧美精品aa| 欧美国产欧美亚洲国产日韩mv天天看完整 | 亚洲国产另类久久久精品极度| 国产精品第13页| 国产精品久久久久影院色老大 | 欧美日韩精品免费观看视一区二区 | 久久国产精品亚洲va麻豆| 久久久之久亚州精品露出| 欧美经典一区二区三区| 一区二区三区在线观看视频| 9色国产精品| 欧美日韩高清不卡| 亚洲午夜小视频| 久久久久久久999精品视频| 国产精品久久99| 亚洲午夜国产成人av电影男同| 久久野战av| 亚洲电影观看| 欧美精品久久久久a| 最新中文字幕一区二区三区| 欧美一级日韩一级| 国产欧美午夜| 久久久www成人免费精品| 国产麻豆9l精品三级站| 另类av导航| 欧美韩日一区| 欧美在线一区二区三区| 亚洲国内自拍| 久久国产精品黑丝| 亚洲国产成人在线视频| 欧美女同在线视频| 一区二区三区国产在线| 国产精品久久午夜夜伦鲁鲁| 一区二区三区 在线观看视| 国产精品午夜国产小视频| 欧美一区午夜精品| 亚洲精品激情| 韩国精品一区二区三区| 欧美日韩在线观看一区二区三区| 亚洲欧美卡通另类91av| 国内精品免费在线观看| 欧美精品入口| 久久视频精品在线| 欧美一区二区三区免费看| 亚洲国产精品黑人久久久| 欧美色欧美亚洲另类七区| 久久免费视频在线观看| 亚洲一区二区三区免费观看| 亚洲国产91精品在线观看| 国产精品久久网| 欧美日韩亚洲不卡| 老牛嫩草一区二区三区日本| 亚洲电影免费在线 | 国产伦理一区| 欧美日韩国产大片| 久久精品国产69国产精品亚洲 | 久久久久国产精品一区| 亚洲永久字幕| 日韩午夜在线播放| 一本一道久久综合狠狠老精东影业| 国内精品久久久久久| 国产亚洲成av人片在线观看桃| 国产精品日产欧美久久久久| 欧美日韩hd| 欧美色中文字幕| 亚洲国产mv| 一色屋精品亚洲香蕉网站| 激情成人亚洲| 亚洲麻豆av| 亚洲一区欧美激情| 久久av在线| 欧美系列一区| 亚洲国产你懂的| 亚洲欧美国产另类| 欧美wwwwww| 国产精品一区二区女厕厕| 激情综合视频| 亚洲午夜羞羞片| 久久亚裔精品欧美| 国产精品自拍一区| 亚洲精品视频二区| 久久精品国产亚洲高清剧情介绍| 国产精品视频大全| 亚洲第一在线综合网站| 亚洲欧美三级伦理| 欧美激情中文字幕乱码免费| 国产网站欧美日韩免费精品在线观看 | 亚洲一品av免费观看| 久久久天天操| 黑人巨大精品欧美一区二区| 亚洲综合色视频| 欧美午夜精品理论片a级大开眼界| 狠狠干成人综合网| 欧美在现视频| 国产精品综合av一区二区国产馆| 影音先锋另类| 欧美不卡一卡二卡免费版| 精品成人国产| 久久久久久久久久久一区| 国产午夜精品视频| 欧美在线观看网址综合| 韩国三级电影一区二区| 久久大逼视频| 亚洲国产日韩美| 欧美日韩综合在线免费观看| 99国产精品久久久久久久成人热 | 亚洲承认在线| 欧美大片免费久久精品三p| 亚洲精品国产视频| 国产精品久久久久久久久免费| 亚洲无线观看| 国产亚洲精品一区二区| 久久伊人一区二区| 一本综合久久| 在线播放日韩欧美| 亚洲成色777777女色窝| 久久激情视频| 亚洲视频在线一区观看| 国产一区在线观看视频| 欧美视频第二页| 欧美岛国激情| 欧美一区二区三区在线观看| 亚洲日本黄色| 伊人久久大香线蕉综合热线 | 国产午夜精品视频| 欧美成人视屏| 久久久久久精| 久久精品免费看| 久久国产视频网站| 亚洲欧美文学| 欧美一级视频一区二区| 亚洲一区二区影院| 欧美日韩国产综合一区二区| 国产亚洲一区二区三区| 欧美精品一区在线观看| 老司机67194精品线观看| 久久精品亚洲精品| 久久久久.com| 久久夜色精品国产| 免费h精品视频在线播放| 久久久噜噜噜久久人人看| 久久久久久香蕉网| 久久综合激情| 欧美调教视频| 国产一区二区精品久久91| 国外成人在线| 亚洲图色在线| 久久一区二区三区国产精品|