什麼是gcd


概述html

我將分四步來帶你們研究研究程序的併發計算。併發

第一步是主要的串行程序,而後使用GCD把它並行計算化。假設你想順着步驟來嘗試這些程序的話,可以下載源代碼。async

注意。別執行imagegcd2.m,這是個反面教材。。函數

  imagegcd.zip (8.4 KB, 79 次)post

 

原始程序ui

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

    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。atom

程序中重要的部分都在這裏了。. Start 函數和 End 函數僅僅是簡單的計時函數(內部實現是使用的gettimeofday函數)。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仍是爲咱們提供了簡單有效的方法來進行併發計算。

相關文章
相關標籤/搜索