什麼是GCD?node
Grand Central Dispatch或者GCD,是一套低層API,提供了一種新的方法來進行併發程序編寫。從基本功能上講,GCD有點像 NSOperationQueue,他們都容許程序將任務切分爲多個單一任務而後提交至工做隊列來併發地或者串行地執行。GCD比之 NSOpertionQueue更底層更高效,而且它不是Cocoa框架的一部分。編程
除了代碼的平行執行能力,GCD還提供高度集成的事件控制系統。能夠設置句柄來響應文件描述符、mach ports(Mach port 用於 OS X上的進程間通信)、進程、計時器、信號、用戶生成事件。這些句柄經過GCD來併發執行。數組
GCD的API很大程度上基於block,固然,GCD也能夠脫離block來使用,好比使用傳統c機制提供函數指針和上下文指針。實踐證實,當配合block使用時,GCD很是簡單易用且能發揮其最大能力。安全
你能夠在Mac上敲命令「man dispatch」來獲取GCD的文檔。多線程
爲什麼使用?併發
GCD提供不少超越傳統多線程編程的優點:app
Dispatch Objects框架
儘管GCD是純c語言的,但它被組建成面向對象的風格。GCD對象被稱爲dispatch object。Dispatch object像Cocoa對象同樣是引用計數的。使用dispatch_release和dispatch_retain函數來操做dispatch object的引用計數來進行內存管理。但注意不像Cocoa對象,dispatch object並不參與垃圾回收系統,因此即便開啓了GC,你也必須手動管理GCD對象的內存。異步
Dispatch queues 和 dispatch sources(後面會介紹到)能夠被掛起和恢復,能夠有一個相關聯的任意上下文指針,能夠有一個相關聯的任務完成觸發函數。能夠查閱「man dispatch_object」來獲取這些功能的更多信息。socket
Dispatch Queues
GCD的基本概念就是dispatch queue。dispatch queue是一個對象,它能夠接受任務,並將任務以先到先執行的順序來執行。dispatch queue能夠是併發的或串行的。併發任務會像NSOperationQueue那樣基於系統負載來合適地併發進行,串行隊列同一時間只執行單一任務。
GCD中有三種隊列類型:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
dispatch_queue_create
建立的隊列. 這些隊列是串行的。正由於如此,它們能夠用來完成同步機制, 有點像傳統線程中的mutex。建立隊列
要使用用戶隊列,咱們首先得建立一個。調用函數dispatch_queue_create就好了。函數的第一個參數是一個標籤,這純是爲了debug。 Apple建議咱們使用倒置域名來命名隊列,好比「com.dreamingwish.subsystem.task」。這些名字會在崩潰日誌中被顯示出 來,也能夠被調試器調用,這在調試中會頗有用。第二個參數目前還不支持,傳入NULL就好了。
提交 Job
向一個隊列提交Job很簡單:調用dispatch_async函數,傳入一個隊列和一個block。隊列會在輪到這個block執行時執行這個block的代碼。下面的例子是一個在後臺執行一個巨長的任務:
dispatch_async
函數會當即返回, block會在後臺異步執行。
固然,一般,任務完成時簡單地NSLog個消息不是個事兒。在典型的Cocoa程序中,你頗有可能但願在任務完成時更新界面,這就意味着須要在主線程中執 行一些代碼。你能夠簡單地完成這個任務——使用嵌套的dispatch,在外層中執行後臺任務,在內層中將任務dispatch到main queue:
還有一個函數叫dispatch_sync,它乾的事兒和dispatch_async相同,可是它會等待block中的代碼執行完成並返回。結合 __block類型修飾符,能夠用來從執行中的block獲取一個值。例如,你可能有一段代碼在後臺執行,而它須要從界面控制層獲取一個值。那麼你可使 用dispatch_sync簡單辦到:
咱們還可使用更好的方法來完成這件事——使用更「異步」的風格。不一樣於取界面層的值時要阻塞後臺線程,你可使用嵌套的block來停止後臺線程,而後從主線程中獲取值,而後再將後期處理提交至後臺線程:
dispatch_queue_t bgQueue = myQueue; dispatch_async(dispatch_get_main_queue(), ^{ NSString *stringValue = [[[textField stringValue] copy] autorelease]; dispatch_async(bgQueue, ^{ // use stringValue in the background now }); });
取決於你的需求,myQueue能夠是用戶隊列也可使全局隊列。
再也不使用鎖(Lock)
用戶隊列能夠用於替代鎖來完成同步機制。在傳統多線程編程中,你可能有一個對象要被多個線程使用,你須要一個鎖來保護這個對象:
NSLock *lock;
訪問代碼會像這樣:
使用GCD,可使用queue來替代:
dispatch_queue_t queue;
要用於同步機制,queue必須是一個用戶隊列,而非全局隊列,因此使用usingdispatch_queue_create
初始化一個。而後能夠用dispatch_async
或者 dispatch_sync
將共享數據的訪問代碼封裝起來:
值得注意的是dispatch queue是很是輕量級的,因此你能夠大用特用,就像你之前使用lock同樣。
如今你可能要問:「這樣很好,可是有意思嗎?我就是換了點代碼辦到了同一件事兒。」
實際上,使用GCD途徑有幾個好處:
-setSomething:是怎麼使用dispatch_async的。調用
-setSomething:會當即返回,而後這一大堆工做會在後臺執行。若是updateSomethingCaches是一個很費時費力的任務,且調用者將要進行一項處理器高負荷任務,那麼這樣作會很棒。總結
如今你已經知道了GCD的基本概念、怎樣建立dispatch queue、怎樣提交Job至dispatch queue以及怎樣將隊列用做線程同步。接下來我會向你展現如何使用GCD來編寫平行執行代碼來充分利用多核系統的性能^ ^。我還會討論GCD更深層的東西,包括事件系統和queue targeting。
概念
爲了在單一進程中充分發揮多核的優點,咱們有必要使用多線程技術(咱們不必去提多進程,這玩意兒和GCD不要緊)。在低層,GCD全局dispatch queue僅僅是工做線程池的抽象。這些隊列中的Block一旦可用,就會被dispatch到工做線程中。提交至用戶隊列的Block最終也會經過全局 隊列進入相同的工做線程池(除非你的用戶隊列的目標是主線程,可是爲了提升運行速度,咱們毫不會這麼幹)。
有兩種途徑來經過GCD「榨取」多核心繫統的性能:將單一任務或者一組相關任務併發至全局隊列中運算;將多個不相關的任務或者關聯不緊密的任務併發至用戶隊列中運算;
全局隊列
設想下面的循環:
1
2
|
for
(
id
obj in array)
[
self
doSomethingIntensiveWith
:obj];
|
假定 -doSomethingIntensiveWith:
是線程安全的且能夠同時執行多個.一個array一般包含多個元素,這樣的話,咱們能夠很簡單地使用GCD來平行運算:
1
2
3
4
5
|
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
for
(
id
obj in array)
dispatch_async(queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
|
如此簡單,咱們已經在多核心上運行這段代碼了。
固然這段代碼並不完美。有時候咱們有一段代碼要像這樣操做一個數組,可是在操做完成後,咱們還須要對操做結果進行其餘操做:
1
2
3
|
for
(
id
obj in array)
[
self
doSomethingIntensiveWith
:obj];
[
self
doSomethingWith
:array];
|
這時候使用GCD的 dispatch_async
就悲劇了.咱們還不能簡單地使用dispatch_sync來解決這個問題
, 由於這將致使每一個迭代器阻塞,就徹底破壞了平行計算。
解決這個問題的一種方法是使用dispatch group。一個dispatch group能夠用來將多個block組成一組以監測這些Block所有完成或者等待所有完成時發出的消息。使用函數 dispatch_group_create來建立,而後使用函數dispatch_group_async來將block提交至一個dispatch queue,同時將它們添加至一個組。因此咱們如今能夠從新編碼:
1
2
3
4
5
6
7
8
9
10
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_group_t group = dispatch_group_create();
for
(
id
obj in array)
dispatch_group_async(group, queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
[
self
doSomethingWith
:array];
|
若是這些工做能夠異步執行,那麼咱們能夠更風騷一點,將函數-doSomethingWith:放在後臺執行。咱們使用dispatch_group_async函數創建一個block在組完成後執行:
1
2
3
4
5
6
7
8
9
10
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_group_t group = dispatch_group_create();
for
(
id
obj in array)
dispatch_group_async(group, queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
dispatch_group_notify(group, queue, ^{
[
self
doSomethingWith
:array];
});
dispatch_release(group);
|
不只全部數組元素都會被平行操做,後續的操做也會異步執行,而且這些異步運算都會將程序的其餘部分的負載考慮在內。注意若是-doSomethingWith:須要在主線程中執行,好比操做GUI,那麼咱們只要將main queue而非全局隊列傳給dispatch_group_notify函數就好了。
對於同步執行,GCD提供了一個簡化方法叫作dispatch_apply。這個函數調用單一block屢次,並平行運算,而後等待全部運算結束,就像咱們想要的那樣:
1
2
3
4
5
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_apply([array
count
], queue, ^(size_t index){
[
self
doSomethingIntensiveWith
:[array
objectAtIndex
:index]];
});
[
self
doSomethingWith
:array];
|
這很棒,可是異步咋辦?dispatch_apply函數但是沒有異步版本的。可是咱們使用的但是一個爲異步而生的API啊!因此咱們只要用dispatch_async函數將全部代碼推到後臺就好了:
1
2
3
4
5
6
7
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_async(queue, ^{
dispatch_apply([array
count
], queue, ^(size_t index){
[
self
doSomethingIntensiveWith
:[array
objectAtIndex
:index]];
});
[
self
doSomethingWith
:array];
});
|
簡單的要死!
這種方法的關鍵在於肯定咱們的代碼是在一次對不一樣的數據片斷進行類似的操做。若是你肯定你的任務是線程安全的(不在本篇討論範圍內)那麼你可使用GCD來重寫你的循環了,更平行更風騷。
要看到性能提高,你還得進行一大堆工做。比之線程,GCD是輕量和低負載的,可是將block提交至queue仍是很消耗資源的——block須要被拷貝 和入隊,同時適當的工做線程須要被通知。不要將一張圖片的每一個像素做爲一個block提交至隊列,GCD的優勢就半途夭折了。若是你不肯定,那麼請進行試 驗。將程序平行計算化是一種優化措施,在修改代碼以前你必須再三思索,肯定修改是有益的(還有確保你修改了正確的地方)。
Subsystem併發運算
前面的章節咱們討論了在程序的單個subsystem中發揮多核心的優點。下來咱們要跨越多個子系統。
例如,設想一個程序要打開一個包含meta信息的文檔。文檔數據自己須要解析並轉換至模型對象來顯示,meta信息也須要解析和轉換。可是,文檔數據和 meta信息不須要交互。咱們能夠爲文檔和meta各建立一個dispatch queue,而後併發執行。文檔和meta的解析代碼都會各自串行執行,從而不用考慮線程安全(只要沒有文檔和meta之間共享的數據),可是它們仍是並 發執行的。
一旦文檔打開了,程序須要響應用戶操做。例如,可能須要進行拼寫檢查、代碼高亮、字數統計、自動保存或者其餘什麼。若是每一個任務都被實現爲在不一樣的 dispatch queue中執行,那麼這些任務會併發執行,並各自將其餘任務的運算考慮在內(respect to each other),從而省去了多線程編程的麻煩。
使用dispatch source(下次我會講到),咱們可讓GCD將事件直接傳遞給用戶隊列。例如,程序中監視socket鏈接的代碼能夠被置於它本身的dispatch queue中,這樣它會異步執行,而且執行時會將程序其餘部分的運算考慮在內。另外,若是使用用戶隊列的話,這個模塊會串行執行,簡化程序。
結論
咱們討論瞭如何使用GCD來提高程序性能以及發揮多核系統的優點。儘管咱們須要比較謹慎地編寫併發程序,GCD仍是使得咱們能更簡單地發揮系統的可用計算資源。
下一篇中,咱們將討論dispatch source,也就是GCD的監視內部、外部事件的機制。
何爲Dispatch Sources
簡單來講,dispatch source是一個監視某些類型事件的對象。當這些事件發生時,它自動將一個block放入一個dispatch queue的執行例程中。
說的貌似有點不清不楚。咱們到底討論哪些事件類型?
下面是GCD 10.6.0版本支持的事件:
這是一堆頗有用的東西,它支持全部kqueue所支持的事件(kqueue是什麼?見http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什麼?見http://en.wikipedia.org/wiki/Mach_(kernel))端口、內建計時器支持(這樣咱們就不用使用超時參數來建立本身的計時器)和用戶事件。
用戶事件
這些事件裏面多數均可以從名字中看出含義,可是你可能想知道啥叫用戶事件。簡單地說,這種事件是由你調用dispatch_source_merge_data函數來向本身發出的信號。
這個名字對於一個發出事件信號的函數來講,太怪異了。這個名字的來由是GCD會在事件句柄被執行以前自動將多個事件進行聯結。你能夠將數據「拼接」至 dispatch source中任意次,而且若是dispatch queue在這期間繁忙的話,GCD只會調用該句柄一次(不要以爲這樣會有問題,看完下面的內容你就明白了)。
用戶事件有兩種: DISPATCH_SOURCE_TYPE_DATA_ADD
和 DISPATCH_SOURCE_TYPE_DATA_OR
.用戶事件源有個 unsigned long data
屬性,咱們將一個 unsigned long
傳入 dispatch_source_merge_data
。當使用 _ADD
版本時,事件在聯結時會把這些數字相加。當使用 _OR
版本時,事件在聯結時會把這些數字邏輯與運算。當事件句柄執行時,咱們可使用dispatch_source_get_data函數訪問當前值,而後這個值會被重置爲0。
讓我假設一種狀況。假設一些異步執行的代碼會更新一個進度條。由於主線程只不過是GCD的另外一個dispatch queue而已,因此咱們能夠將GUI更新工做push到主線程中。然而,這些事件可能會有一大堆,咱們不想對GUI進行頻繁而累贅的更新,理想的狀況是 當主線程繁忙時將全部的改變聯結起來。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,咱們能夠將工做拼接起來,而後主線程能夠知道從上一次處理完事件到如今一共發生了多少改變,而後將這一整段改變一次更新至進度條。
啥也不說了,上代碼:
(對於這段代碼,我很想說點什麼,我第一次用dispatch source時,我糾結了好久好久,真讓人蛋疼:Dispatch source啓動時默認狀態是掛起的,咱們建立完畢以後得主動恢復,不然事件不會被傳遞,也不會被執行)
假設你已經將進度條的min/max值設置好了,那麼這段代碼就完美了。數據會被併發處理。當每一段數據完成後,會通知dispatch source並將dispatch source data加1,這樣咱們就認爲一個單元的工做完成了。事件句柄根據已完成的工做單元來更新進度條。若主線程比較空閒而且這些工做單元進行的比較慢,那麼事 件句柄會在每一個工做單元完成的時候被調用,實時更新。若是主線程忙於其餘工做,或者工做單元完成速度很快,那麼完成事件會被聯結起來,致使進度條只在主線 程變得可用時才被更新,而且一次將積累的改變動新至GUI。
如今你可能會想,聽起來卻是不錯,可是要是我不想讓事件被聯結呢?有時候你可能想讓每一次信號都會引發響應,什麼後臺的智能玩意兒通通不要。啊。。其實很 簡單的,別把本身繞進去了。若是你想讓每個信號都獲得響應,那使用dispatch_async函數不就好了。實際上,使用的dispatch source而不使用dispatch_async的惟一緣由就是利用聯結的優點。
內建事件
上面就是怎樣使用用戶事件,那麼內建事件呢?看看下面這個例子,用GCD讀取標準輸入:
簡單的要死!由於咱們使用的是全局隊列,句柄自動在後臺執行,與程序的其餘部分並行,這意味着對這種狀況的提速:事件進入程序時,程序正在處理其餘事務。
這是標準的UNIX方式來處理事務的好處,不用去寫loop。若是使用經典的 read
調用,咱們還得萬分留神,由於返回的數據可能比請求的少,還得忍受無厘頭的「errors」,好比 EINTR
(系統調用中斷)。使用GCD,咱們啥都不用管,就從這些蛋疼的狀況裏解脫了。若是咱們在文件描述符中留下了未讀取的數據,GCD會再次調用咱們的句柄。
對於標準輸入,這沒什麼問題,可是對於其餘文件描述符,咱們必須考慮在完成讀寫以後怎樣清除描述符。對於dispatch source還處於活躍狀態時,咱們決不能關閉描述符。若是另外一個文件描述符被建立了(多是另外一個線程建立的)而且新的描述符恰好被分配了相同的數字, 那麼你的dispatch source可能會在不該該的時候忽然進入讀寫狀態。de這個bug可不是什麼好玩的事兒。
適當的清除方式是使用 dispatch_source_set_cancel_handler
,並傳入一個block來關閉文件描述符。而後咱們使用 dispatch_source_cancel
來取消dispatch source,使得句柄被調用,而後文件描述符被關閉。
使用其餘dispatch source類型也差很少。總的來講,你提供一個source(mach port、文件描述符、進程ID等等)的區分符來做爲diapatch source的句柄。mask參數一般不會被使用,可是對於 DISPATCH_SOURCE_TYPE_PROC
來講mask指的是咱們想要接受哪種進程事件。而後咱們提供一個句柄,而後恢復這個source(前面我加粗字體所說的,得先恢復),搞定。dispatch source也提供一個特定於source的data,咱們使用 dispatch_source_get_data
函數來訪問它。例如,文件描述符會給出大體可用的字節數。進程source會給出上次調用以後發生的事件的mask。具體每種source給出的data的含義,看man page吧。
計時器
計時器事件稍有不一樣。它們不使用handle/mask參數,計時器事件使用另一個函數 dispatch_source_set_timer
來配置計時器。這個函數使用三個參數來控制計時器觸發:
start
參數控制計時器第一次觸發的時刻。參數類型是 dispatch_time_t
,這是一個opaque類型,咱們不能直接操做它。咱們得須要dispatch_time
和 dispatch_walltime
函數來建立它們。另外,常量 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
一般頗有用。
interval
參數沒什麼好解釋的。
leeway
參數比較有意思。這個參數告訴系統咱們須要計時器觸發的精準程度。全部的計時器都不會保證100%精準,這個參 數用來告訴系統你但願系統保證精準的努力程度。若是你但願一個計時器沒五秒觸發一次,而且越準越好,那麼你傳遞0爲參數。另外,若是是一個週期性任務,比 如檢查email,那麼你會但願每十分鐘檢查一次,可是不用那麼精準。因此你能夠傳入60,告訴系統60秒的偏差是可接受的。
這樣有什麼意義呢?簡單來講,就是下降資源消耗。若是系統可讓cpu休息足夠長的時間,並在每次醒來的時候執行一個任務集合,而不是不斷的醒來睡去以執 行任務,那麼系統會更高效。若是傳入一個比較大的leeway給你的計時器,意味着你容許系統拖延你的計時器來將計時器任務與其餘任務聯合起來一塊兒執行。
總結
如今你知道怎樣使用GCD的dispatch source功能來監視文件描述符、計時器、聯結的用戶事件以及其餘相似的行爲。因爲dispatch source徹底與dispatch queue相集成,因此你可使用任意的dispatch queue。你能夠將一個dispatch source的句柄在主線程中執行、在全局隊列中併發執行、或者在用戶隊列中串行執行(執行時會將程序的其餘模塊的運算考慮在內)。
下一篇我會討論如何對dispatch queue進行掛起、恢復、重定目標操做;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能。
Dispatch Queue掛起
dispatch queue能夠被掛起和恢復。使用 dispatch_suspend
函數來掛起,使用 dispatch_resume
函數來恢復。這兩個函數的行爲是如你所願的。另外,這兩個函數也能夠用於dispatch source。
一個要注意的地方是,dispatch queue的掛起是block粒度的。換句話說,掛起一個queue並不會將當前正在執行的block掛起。它會容許當前執行的block執行完畢,而後後續的block再也不會被執行,直至queue被恢復。
還有一個注意點:從man頁上得來的:若是你掛起了一個queue或者source,那麼銷燬它以前,必須先對其進行恢復。
Dispatch Queue目標指定
全部的用戶隊列都有一個目標隊列概念。從本質上講,一個用戶隊列其實是不執行任何任務的,可是它會將任務傳遞給它的目標隊列來執行。一般,目標隊列是默認優先級的全局隊列。
用戶隊列的目標隊列能夠用函數 dispatch_set_target_queue
來 修改。咱們能夠將任意dispatch queue傳遞給這個函數,甚至能夠是另外一個用戶隊列,只要別構成循環就行。這個函數能夠用來設定用戶隊列的優先級。好比咱們能夠將用戶隊列的目標隊列設 定爲低優先級的全局隊列,那麼咱們的用戶隊列中的任務都會以低優先級執行。高優先級也是同樣道理。
有一個用途,是將用戶隊列的目標定爲main queue。這會致使全部提交到該用戶隊列的block在主線程中執行。這樣作來替代直接在主線程中執行代碼的好處在於,咱們的用戶隊列能夠單獨地被掛起和恢復,還能夠被重定目標至一個全局隊列,而後全部的block會變成在全局隊列上執行(只要你確保你的代碼離開主線程不會有問題)。
還有一個用途,是將一個用戶隊列的目標隊列指定爲另外一個用戶隊列。這樣作能夠強制多個隊列相互協調地串行執行,這樣足以構建一組隊列,經過掛起和暫停那個 目標隊列,咱們能夠掛起和暫停整個組。想象這樣一個程序:它掃描一組目錄而且加載目錄中的內容。爲了不磁盤競爭,咱們要肯定在同一個物理磁盤上同時只有 一個文件加載任務在執行。而但願能夠同時從不一樣的物理磁盤上讀取多個文件。要實現這個,咱們要作的就是建立一個dispatch queue結構,該結構爲磁盤結構的鏡像。
首先,咱們會掃描系統並找到各個磁盤,爲每一個磁盤建立一個用戶隊列。而後掃描文件系統,併爲每一個文件系統建立一個用戶隊列,將這些用戶隊列的目標隊列指向 合適的磁盤用戶隊列。最後,每一個目錄掃描器有本身的隊列,其目標隊列指向目錄所在的文件系統的隊列。目錄掃描器枚舉本身的目錄併爲每一個文件向本身的隊列提 交一個block。因爲整個系統的創建方式,就使得每一個物理磁盤被串行訪問,而多個物理磁盤被並行訪問。除了隊列初始化過程,咱們根本不須要手動干預什麼 東西。
信號量
dispatch的信號量是像其餘的信號量同樣的,若是你熟悉其餘多線程系統中的信號量,那麼這一節的東西再好理解不過了。
信號量是一個整形值而且具備一個初始計數值,而且支持兩個操做:信號通知和等待。當一個信號量被信號通知,其計數會被增長。當一個線程在一個信號量上等待時,線程會被阻塞(若是有必要的話),直至計數器大於零,而後線程會減小這個計數。
咱們使用函數 dispatch_semaphore_create
來建立dispatch信號量,使用函數 dispatch_semaphore_signal
來信號通知,使用函數dispatch_semaphore_wait
來等待。這些函數的man頁有兩個很好的例子,展現了怎樣使用信號量來同步任務和有限資源訪問控制。
單次初始化
GCD還提供單次初始化支持,這個與pthread中的函數 pthread_once
很類似。GCD提供的方式的優勢在於它使用block而非函數指針,這就容許更天然的代碼方式:
這個特性的主要用途是惰性單例初始化或者其餘的線程安全數據共享。典型的單例初始化技術看起來像這樣(線程安全的):
這挺好的,可是代價比較昂貴;每次調用 +sharedWhatever
函數都會付出取鎖的代價,即便這個鎖只須要進行一次。確實有更風騷的方式來實現這個,使用相似雙向鎖或者是原子操做的東西,可是這樣挺難弄並且容易出錯。
使用GCD,咱們能夠這樣重寫上面的方法,使用函數 dispatch_once
:
這個稍微比 @synchronized
方法簡單些,而且GCD確保以更快的方式完成這些檢測,它保證block中的代碼在任何線程經過 dispatch_once
調用以前被執行,但它不會強制每次調用這個函數都讓代碼進行同步控制。實際上,若是你去看這個函數所在的頭文件,你會發現目前它的實現實際上是一個宏,進行了內聯的初始化測試,這意味着一般狀況下,你不用付出函數調用的負載代價,而且會有更少的同步控制負載。
結論
這一章,咱們介紹了dispatch queue的掛起、恢復和目標重定,以及這些功能的一些用途。另外,咱們還介紹瞭如何使用dispatch 信號量和單次初始化功能。到此,我已經完成了GCD如何運做以及如何使用的介紹。
其主要思路是使用gcd建立串行隊列,而後在此隊列中前後執行兩個任務:1.預加載一個viewController 2.將這個viewController推入
代碼以下:
概述
我將分四步來帶你們研究研究程序的併發計算。第一步是基本的串行程序,而後使用GCD把它並行計算化。若是你想順着步驟來嘗試這些程序的話,能夠下載源碼。注意,別運行imagegcd2.m,這是個反面教材。。
原始程序
咱們的程序只是簡單地遍歷~/Pictures而後生成縮略圖。這個程序是個命令行程序,沒有圖形界面(儘管是使用Cocoa開發庫的),主函數以下:
若是你要看到全部的副主函數的話,到文章頂部下載源代碼吧。當前這個程序是imagegcd1.m。程序中重要的部分都在這裏了。. Start
函數和 End
函數只是簡單的計時函數(內部實現是使用的gettimeofday函數
)。ThumbnailDataForData函數使用Cocoa庫來加載圖片數據生成Image對象,而後將圖片縮小到320×320大小,最後將其編碼爲JPEG格式。
簡單而天真的併發
乍一看,咱們感受將這個程序併發計算化,很容易。循環中的每一個迭代器均可以放入GCD global queue中。咱們可使用dispatch queue來等待它們完成。爲了保證每次迭代都會獲得惟一的文件名數字,咱們使用OSAtomicIncrement32來原子操做級別的增長count 數:
這個就是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專門用來讀寫磁盤):
這個就是咱們的 imagegcd3.m
.
GCD使得咱們很容易就將任務的不一樣部分放入相同的隊列中去(簡單地嵌套一下dispatch)。此次咱們的程序將會表現地很好。。。我是說多數狀況。。。。
問題在於任務中的不一樣部分不是同步的,致使了整個程序的不穩定。咱們的新程序的整個流程以下:
Main Thread IO Queue Concurrent Queue find paths ------> read -----------> process ... write <----------- process
圖中的箭頭是非阻塞的,而且會簡單地將內存中的對象進行緩衝。
如今假設一個機器的磁盤足夠快,快到比CPU處理任務(也就是圖片處理)要快。其實不難想象:雖然CPU的動做很快,可是它的工做更繁重,解碼、壓縮、 編碼。從磁盤讀取的數據開始填滿IO queue,數據會佔用內存,極可能越佔越多(若是你的~/Pictures中有不少不少圖片的話)。
而後你就會內存爆倉,而後開始虛擬內存交換。。。又來了。。
這就會像第一次同樣致使惡性循環。一旦任何東西致使工做線程阻塞,GCD就會建立更多的線程,這個線程執行的任務又會佔用內存(從磁盤讀取的數據),而後又開始交換內存。。
結果:這個程序要麼就是運行地很順暢,要麼就是很低效。
注意若是磁盤速度比較慢的話,這個問題依舊會出現,由於縮略圖會被緩衝在內存裏,不過這個問題致使的低效比較不容易出現,由於縮略圖佔的內存少得多。
真正的修復
因爲上一次咱們的嘗試出現的問題在於沒有同步不一樣部分的操做,因此讓我寫出同步的代碼。最簡單的方法就是使用信號量來限制同時執行的任務數量。
那麼,咱們須要限制爲多少呢?
顯然咱們須要根據CPU的核數來限制這個量,咱們又想馬兒好又想馬兒不吃草,咱們就設置爲cpu核數的兩倍吧。不過這裏只是簡單地這樣處理,GCD的做用 之一就是讓咱們不用關心操做系統的內部信息(好比cpu數),如今又來讀取cpu核數,確實不太妙。也許咱們在實際應用中,能夠根據其餘需求來定義這個限 制量。
如今咱們的主循環代碼就是這樣了:
最終咱們寫出了一個能平滑運行且又快速處理的程序。
基準測試
我測試了一些運行時間,對7913張圖片:
程序處理時間 (秒)
imagegcd1.m |
984 |
imagegcd2.m |
沒運行,這個仍是別運行了 |
imagegcd3.m |
300 |
imagegcd4.m |
279 |
注意,由於我比較懶。因此我在運行這些測試的時候,沒有關閉電腦上的其餘程序。。。嚴格的進行對照的話,實在是太蛋疼了。。
因此這個數值咱們只是參考一下。
比較有意思的是,3和4的執行情況差很少,大概是由於我電腦有15g可用內存吧。。。內存比較小的話,這個imagegcd3應該跑的很吃力,由於我發現它使用最多的時候,佔用了10g內存。而4的話,沒有佔多少內存。
結論
GCD是個比較范特西的技術,能夠辦到不少事兒,可是它不能爲你辦全部的事兒。因此,對於進行IO操做而且可能會使用大量內存的任務,咱們必須仔細斟酌。固然,即便這樣,GCD仍是爲咱們提供了簡單有效的方法來進行併發計算。