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仍是爲咱們提供了簡單有效的方法來進行併發計算。