什麼是gcd

版權聲明:本文爲博主原創文章。未經博主贊成不得轉載。

https://blog.csdn.net/coolwu123/article/details/28615029 css


概述html

我將分四步來帶你們研究研究程序的併發計算。第一步是主要的串行程序,而後使用GCD把它並行計算化。假設你想順着步驟來嘗試這些程序的話,可以下載源代碼。注意。別執行imagegcd2.m,這是個反面教材。併發

async

  imagegcd.zip (8.4 KB, 79 次)函數

 

原始程序post

咱們的程序僅僅是簡單地遍歷~/Pictures而後生成縮略圖。這個程序是個命令行程序,沒有圖形界面(雖然是使用Cocoa開發庫的)。主函數例如如下:ui

int main(int argc, char **argv)
    {
        NSAutoreleasePool *outerPool = [NSAutoreleasePool new];
        
        NSApplicationLoad();
        
        NSString *destination = @"/tmp/imagegcd";
        [[NSFileManager defaultManager] removeItemAtPath: destination error: NULL];
        [[NSFileManager defaultManager] createDirectoryAtPath: destination
                                        withIntermediateDirectories: YES
                                        attributes: nil
                                        error: NULL];
        
        
        Start();
        
        NSString *dir = [@"~/Pictures" stringByExpandingTildeInPath];
        NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath: dir];
        int count = 0;
        for(NSString *path in enumerator)
        {
            NSAutoreleasePool *innerPool = [NSAutoreleasePool new];
            
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                path = [dir stringByAppendingPathComponent: path];
                
                NSData *data = [NSData dataWithContentsOfFile: path];
                if(data)
                {
                    NSData *thumbnailData = ThumbnailDataForData(data);
                    if(thumbnailData)
                    {
                        NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg", count++];
                        NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                        [thumbnailData writeToFile: thumbnailPath atomically: NO];
                    }
                }
            }
            
            [innerPool release];
        }
        
        End();
        
        [outerPool release];
    }
 

假設你要看到所有的副主函數的話,到文章頂部下載源代碼吧。當前這個程序是imagegcd1.m。編碼

程序中重要的部分都在這裏了。. Start 函數和 End 函數僅僅是簡單的計時函數(內部實現是使用的gettimeofday函數)。atom

ThumbnailDataForData函數使用Cocoa庫來載入圖片數據生成Image對象。而後將圖片縮小到320×320大小。最後將其編碼爲JPEG格式。spa

 

簡單而天真的併發

乍一看。咱們感受將這個程序併發計算化,很是easy。循環中的每個迭代器都可以放入GCD global queue中。

咱們可以使用dispatch queue來等待它們完畢。爲了保證每次迭代都會獲得惟一的文件名稱數字,咱們使用OSAtomicIncrement32來原子操做級別的添加count數:

dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                NSString *fullPath = [dir stringByAppendingPathComponent: path];
                
                NSData *data = [NSData dataWithContentsOfFile: fullPath];
                if(data)
                {
                    NSData *thumbnailData = ThumbnailDataForData(data);
                    if(thumbnailData)
                    {
                        NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                   OSAtomicIncrement32(&count;)];
                        NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                        [thumbnailData writeToFile: thumbnailPath atomically: NO];
                    }
                }
            }
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

這個就是imagegcd2.m。但是,注意,別執行這個程序。有很是大的問題。 

假設你無視個人警告仍是執行這個imagegcd2.m了,你現在很是有多是在從新啓動了電腦後,又打開了個人頁面。。假設你乖乖地沒有執行這個程序的話,執行這個程序發生的狀況就是(假設你有很是多很是多圖片在~/Pictures中):電腦沒反應。很久很久都不動,假死了。。

 

問題在哪

問題出在哪?就在於GCD的智能上。GCD將任務放到全局線程池中執行,這個線程池的大小依據系統負載來隨時改變。好比,個人電腦有四核,因此假設我使用GCD載入任務。GCD會爲我每個cpu核建立一個線程。也就是四個線程。假設電腦上其它任務需要進行的話。GCD會下降線程數來使其它任務得以佔用cpu資源來完畢。

但是,GCD也可以添加活動線程數。

它會在其它某個線程堵塞時添加活動線程數。假設現在有四個線程正在執行,忽然某個線程要作一個操做,比方,讀文件。這個線程就會等待磁盤響應,此時cpu核心會處於未充分利用的狀態。這是GCD就會發現這個狀態。而後建立還有一個線程來填補這個資源浪費空缺。

現在。想一想上面的程序發生了啥?主線程很是迅速地將任務不斷放入global queue中。GCD以一個少許工做線程的狀態開始。而後開始執行任務。這些任務執行了一些很是輕量的工做後。就開始等待磁盤資源,慢得不像話的磁盤資源。

咱們別忘記磁盤資源的特性。除非你使用的是SSD或者牛逼的RAID。不然磁盤資源會在競爭的時候變得異常的慢。

剛開始的四個任務很是輕鬆地就同一時候訪問到了磁盤資源,而後開始等待磁盤資源返回。這時GCD發現CPU開始空暇了,它繼續添加工做線程。

而後。這些線程執行不少其它的磁盤讀取任務,而後GCD再建立不少其它的工資線程。。。

可能在某個時間文件讀取任務有完畢的了。現在,線程池中可不止有四個線程,相反。有成百上千個。。。

GCD又會嘗試將工做線程下降(太多使用CPU資源的線程),但是下降線程是由條件的。GCD不可以將一個正在執行任務的線程殺掉,並且也不能將這種任務暫停。它必須等待這個任務完畢。所有這些狀況都致使GCD沒法下降工做線程數。

而後所有這上百個線程開始一個個完畢了他們的磁盤讀取工做。

它們開始競爭CPU資源。固然CPU在處理競爭上比磁盤先進多了。

問題在於,這些線程讀完文件後開始編碼這些圖片。假設你有很是多很是多圖片。那麼你的內存將開始爆倉。。而後內存耗盡咋辦?虛擬內存啊。虛擬內存是啥,磁盤資源啊。

Oh shit!~

而後進入了一個惡性循環。磁盤資源競爭致使不少其它的線程被建立。這些線程致使不少其它的內存使用,而後內存爆倉致使虛擬內存交換。直至GCD建立了系統規定的線程數上限(多是512個),而這些線程又無法被殺掉或暫停。

這就是使用GCD時。要注意的。GCD能智能地依據CPU狀況來調整工做線程數,但是它卻沒法監視其它類型的資源情況。假設你的任務牽涉大量IO或者其它會致使線程block的東西,你需要把握好這個問題。

 

修正
問題的根源來自於磁盤IO,而後致使惡性循環。

攻克了磁盤資源碰撞,就攻克了這個問題。

GCD的custom queue使得這個問題易於解決。

Custom queue是串行的。假設咱們建立一個custom queue而後將所有的文件讀寫任務放入這個隊列,磁盤資源的同一時候訪問數會大大下降。資源訪問碰撞就避免了。

蝦米是咱們修正後的代碼,使用IO queue(也就是咱們建立的custom queue專門用來讀寫磁盤):

dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t ioQueue = dispatch_queue_create("com.mikeash.imagegcd.io", NULL);
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
        {
            NSString *fullPath = [dir stringByAppendingPathComponent: path];
            
            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                NSData *data = [NSData dataWithContentsOfFile: fullPath];
                if(data)
                    dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
                        NSData *thumbnailData = ThumbnailDataForData(data);
                        if(thumbnailData)
                        {
                            NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                       OSAtomicIncrement32(&count;)];
                            NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                                [thumbnailData writeToFile: thumbnailPath atomically: NO];
                            }));
                        }
                    }));
            }));
        }
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

 這個就是咱們的 imagegcd3.m.

GCD使得咱們很是easy就將任務的不一樣部分放入一樣的隊列中去(簡單地嵌套一下dispatch)。此次咱們的程序將會表現地很是好。。

。我是說多數狀況。。。。

問題在於任務中的不一樣部分不是同步的。致使了整個程序的不穩定。

咱們的新程序的整個流程例如如下:

    Main Thread          IO Queue            Concurrent Queue
    
    find paths  ------>  read  ----------->  process
                                             ...
                         write <-----------  process

圖中的箭頭是非堵塞的,並且會簡單地將內存中的對象進行緩衝。

 

 現在假設一個機器的磁盤足夠快,快到比CPU處理任務(也就是圖片處理)要快。

事實上不難想象:雖然CPU的動做很是快,但是它的工做更繁重,解碼、壓縮、編碼。從磁盤讀取的數據開始填滿IO queue,數據會佔用內存,很是可能越佔越多(假設你的~/Pictures中有很是多很是多圖片的話)。

而後你就會內存爆倉,而後開始虛擬內存交換。

。。又來了。

這就會像第一次同樣致使惡性循環。一旦不論什麼東西致使工做線程堵塞。GCD就會建立不少其它的線程,這個線程執行的任務又會佔用內存(從磁盤讀取的數據),而後又開始交換內存。

結果:這個程序要麼就是執行地很是順暢,要麼就是很是低效。

注意假設磁盤速度比較慢的話。這個問題依然會出現。因爲縮略圖會被緩衝在內存裏,只是這個問題致使的低效比較不easy出現,因爲縮略圖佔的內存少得多。

 

真正的修復

因爲上一次咱們的嘗試出現的問題在於沒有同步不一樣部分的操做,因此讓我寫出同步的代碼。最簡單的方法就是使用信號量來限制同一時候執行的任務數量。

那麼,咱們需要限制爲多少呢?

顯然咱們需要依據CPU的核數來限制這個量,咱們又想馬兒好又想馬兒不吃草。咱們就設置爲cpu核數的兩倍吧。只是這裏僅僅是簡單地這樣處理。GCD的做用之中的一個就是讓咱們不用關心操做系統的內部信息(比方cpu數),現在又來讀取cpu核數。確實不太妙。或許咱們在實際應用中,可以依據其它需求來定義這個限制量。

現在咱們的主循環代碼就是這樣了:

dispatch_queue_t ioQueue = dispatch_queue_create("com.mikeash.imagegcd.io", NULL);
    
    int cpuCount = [[NSProcessInfo processInfo] processorCount];
    dispatch_semaphore_t jobSemaphore = dispatch_semaphore_create(cpuCount * 2);
    
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        WithAutoreleasePool(^{
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                NSString *fullPath = [dir stringByAppendingPathComponent: path];
                
                dispatch_semaphore_wait(jobSemaphore, DISPATCH_TIME_FOREVER);
            
                dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                    NSData *data = [NSData dataWithContentsOfFile: fullPath];
                    dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
                        NSData *thumbnailData = ThumbnailDataForData(data);
                        if(thumbnailData)
                        {
                            NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                       OSAtomicIncrement32(&count;)];
                            NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                                [thumbnailData writeToFile: thumbnailPath atomically: NO];
                                dispatch_semaphore_signal(jobSemaphore);
                            }));
                        }
                        else
                            dispatch_semaphore_signal(jobSemaphore);
                    }));
                }));
            }
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

終於咱們寫出了一個能平滑執行且又高速處理的程序。

 

基準測試

我測試了一些執行時間,對7913張圖片:

 

程序處理時間 (秒)
imagegcd1.m 984
imagegcd2.m 沒執行,這個仍是別執行了
imagegcd3.m 300
imagegcd4.m 279

 

 

注意,因爲我比較懶。

因此我在執行這些測試的時候,沒有關閉電腦上的其它程序。。

。嚴格的進行對比的話,實在是太蛋疼了。。

因此這個數值咱們僅僅是參考一下。

比較有意思的是。3和4的執行情況幾乎相同,大概是因爲我電腦有15g可用內存吧。。。內存比較小的話,這個imagegcd3應該跑的很是吃力。因爲我發現它使用最多的時候,佔用了10g內存。而4的話。沒有佔多少內存。

結論

GCD是個比較范特西的技術,可以辦到很是多事兒,但是它不能爲你辦所有的事兒。

因此,對於進行IO操做並且可能會使用大量內存的任務。咱們必須細緻斟酌。

固然,即便這樣,GCD仍是爲咱們提供了簡單有效的方法來進行併發計算。

相關文章
相關標籤/搜索