概念:
程序中同步和異步是什么意思?有什么區別?
串行執行相當于同步
并發執行相當于異步
GCD介紹(一): 基本概念和Dispatch Queue
GCD提供很多超越傳統多線程編程的優勢:
易用:Dispatch Objects
GCD對象被稱為dispatch object。Dispatch object像Cocoa對象一樣是引用計數的。使用dispatch_release和dispatch_retain函數來操作dispatch object的引用計數來進行內存管理。
但注意不像Cocoa對象,dispatch object并不參與垃圾回收系統,所以即使開啟了ARC,你也必須手動管理GCD對象的內存。
Dispatch queues 和 dispatch sources(后面會介紹到)可以被掛起和恢復,可以有一個相關聯的任意上下文指針,可以有一個相關聯的任務完成觸發函數。
Dispatch Queues
GCD的基本概念就是dispatch queue。dispatch queue是一個對象,它可以接受任務,并將任務以先到先執行的順序來執行。dispatch queue可以是并發的或串行的。并發任務會像NSOperationQueue那樣基于系統負載來合適地并發進行,串行隊列同一時間只執行單一任務。
GCD中有三種隊列類型:
The main queue:創建隊列
要使用用戶隊列,我們首先得創建一個。調用函數dispatch_queue_create就行了。函數的第一個參數是一個標簽,這純是為了debug。Apple建議我們使用倒置域名來命名隊列,比如“com.dreamingwish.subsystem.task”。這些名字會在崩潰日志中被顯示出來,也可以被調試器調用,這在調試中會很有用。第二個參數目前還不支持,傳入NULL就行了。
提交 Job
向一個隊列提交Job很簡單:調用dispatch_async函數,傳入一個隊列和一個block。隊列會在輪到這個block執行時執行這個block的代碼。下面的例子是一個在后臺執行一個巨長的任務:
});
dispatch_async
當然,通常,任務完成時簡單地NSLog個消息不是個事兒。在典型的Cocoa程序中,你很有可能希望在任務完成時更新界面,這就意味著需要在主線 程中執行一些代碼。你可以簡單地完成這個任務——使用嵌套的dispatch,在外層中執行后臺任務,在內層中將任務dispatch到main queue:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
還有一個函數叫dispatch_sync,它干的事兒和dispatch_async相同,但是它會等待block中的代碼執行完成并返回。結合 __block類型修飾符,可以用來從執行中的block獲取一個值。例如,你可能有一段代碼在后臺執行,而它需要從界面控制層獲取一個值。那么你可以使用dispatch_sync簡單辦到:
__block NSString *stringValue;
dispatch_sync(dispatch_get_main_queue(), ^{
});
[stringValue autorelease];
// use stringValue in the background now
我們還可以使用更好的方法來完成這件事——使用更“異步”的風格。不同于取界面層的值時要阻塞后臺線程,你可以使用嵌套的block來中止后臺線程,然后從主線程中獲取值,然后再將后期處理提交至后臺線程:
取決于你的需求,myQueue可以是用戶隊列也可以使全局隊列。
不再使用鎖(Lock)
用戶隊列可以用于替代鎖來完成同步機制。在傳統多線程編程中,你可能有一個對象要被多個線程使用,你需要一個鎖來保護這個對象:
訪問代碼會像這樣:
使用GCD,可以使用queue來替代:
要用于同步機制,queue必須是一個用戶隊列,而非全局隊列,所以使用usingdispatch_queue_create初始化一個。然后可以用dispatch_async
現在你可能要問:“這樣很好,但是有意思嗎?我就是換了點代碼辦到了同一件事兒。”
實際上,使用GCD途徑有幾個好處:
平行計算:總結
現在你已經知道了GCD的基本概念、怎樣創建dispatch queue、怎樣提交Job至dispatch queue以及怎樣將隊列用作線程同步。接下來我會向你展示如何使用GCD來編寫平行執行代碼來充分利用多核系統的性能^ ^。我還會討論GCD更深層的東西,包括事件系統和queue targeting。
GCD介紹(二): 多核心的性能
概念
為了在單一進程中充分發揮多核的優勢,我們有必要使用多線程技術(我們沒必要去提多進程,這玩意兒和GCD沒關系)。在低層,GCD全局 dispatch queue僅僅是工作線程池的抽象。這些隊列中的Block一旦可用,就會被dispatch到工作線程中。提交至用戶隊列的Block最終也會通過全局 隊列進入相同的工作線程池(除非你的用戶隊列的目標是主線程,但是為了提高運行速度,我們絕不會這么干)。
有兩種途徑來通過GCD“榨取”多核心系統的性能:將單一任務或者一組相關任務并發至全局隊列中運算;將多個不相關的任務或者關聯不緊密的任務并發至用戶隊列中運算;
全局隊列
設想下面的循環:
1 2 | for(id obj in array) |
假定
1 2 3 4 5 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for(id obj in array) |
如此簡單,我們已經在多核心上運行這段代碼了。
當然這段代碼并不完美。有時候我們有一段代碼要像這樣操作一個數組,但是在操作完成后,我們還需要對操作結果進行其他操作:
1 2 3 | for(id obj in array) [self doSomethingWith:array]; |
這時候使用GCD的
解決這個問題的一種方法是使用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_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_notify(group, queue, ^{ }); 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函數可是沒有異步版本的。但是我們使用的可是一個為異步而生的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, ^{ }); |
簡單的要死!
這種方法的關鍵在于確定我們的代碼是在一次對不同的數據片段進行相似的操作。如果你確定你的任務是線程安全的(不在本篇討論范圍內)那么你可以使用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的監視內部、外部事件的機制。
GCD介紹(三): Dispatch Sources
何為Dispatch Sources
簡單來說,dispatch source是一個監視某些類型事件的對象。當這些事件發生時,它自動將一個block放入一個dispatch queue的執行例程中。
說的貌似有點不清不楚。我們到底討論哪些事件類型?
下面是GCD 10.6.0版本支持的事件:
Mach port send right state changes.Mach port receive right state changes.External process state change.File descriptor ready for read.File descriptor ready for write.Filesystem node event.POSIX signal.Custom timer.Custom event.這是一堆很有用的東西,它支持所有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只會調用該句柄一次(不要覺得這樣會有問題,看完下面的內容你就明白了)。
用戶事件有兩種:
讓我假設一種情況。假設一些異步執行的代碼會更新一個進度條。因為主線程只不過是GCD的另一個dispatch queue而已,所以我們可以將GUI更新工作push到主線程中。然而,這些事件可能會有一大堆,我們不想對GUI進行頻繁而累贅的更新,理想的情況是 當主線程繁忙時將所有的改變聯結起來。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我們可以將工作拼接起來,然后主線程可以知道從上一次處理完事件到現在一共發生了多少改變,然后將這一整段改變一次更新至進度條。
啥也不說了,上代碼:
假設你已經將進度條的min/max值設置好了,那么這段代碼就完美了。數據會被并發處理。當每一段數據完成后,會通知dispatch source并將dispatch source data加1,這樣我們就認為一個單元的工作完成了。事件句柄根據已完成的工作單元來更新進度條。若主線程比較空閑并且這些工作單元進行的比較慢,那么事 件句柄會在每個工作單元完成的時候被調用,實時更新。如果主線程忙于其他工作,或者工作單元完成速度很快,那么完成事件會被聯結起來,導致進度條只在主線 程變得可用時才被更新,并且一次將積累的改變更新至GUI。
現在你可能會想,聽起來倒是不錯,但是要是我不想讓事件被聯結呢?有時候你可能想讓每一次信號都會引起響應,什么后臺的智能玩意兒統統不要。啊。。 其實很簡單的,把你的思想放到禁錮的框子之外就行了。如果你想讓每一個信號都得到響應,那使用dispatch_async函數不就行了。實際上,使用的 dispatch source而不使用dispatch_async的唯一原因就是利用聯結的優勢。
內建事件
上面就是怎樣使用用戶事件,那么內建事件呢?看看下面這個例子,用GCD讀取標準輸入:
這是標準的UNIX方式來處理事務的好處,不用去寫loop。如果使用經典的
對于標準輸入,這沒什么問題,但是對于其他文件描述符,我們必須考慮在完成讀寫之后怎樣清除描述符。對于dispatch source還處于活躍狀態時,我們決不能關閉描述符。如果另一個文件描述符被創建了(可能是另一個線程創建的)并且新的描述符剛好被分配了相同的數字, 那么你的dispatch source可能會在不應該的時候突然進入讀寫狀態。de這個bug可不是什么好玩的事兒。
適當的清除方式是使用
使用其他dispatch source類型也差不多。總的來說,你提供一個source(mach port、文件描述符、進程ID等等)的區分符來作為diapatch source的句柄。mask參數通常不會被使用,但是對于
計時器
計時器事件稍有不同。它們不使用handle/mask參數,計時器事件使用另外一個函數
這樣有什么意義呢?簡單來說,就是降低資源消耗。如果系統可以讓cpu休息足夠長的時間,并在每次醒來的時候執行一個任務集合,而不是不斷的醒來睡 去以執行任務,那么系統會更高效。如果傳入一個比較大的leeway給你的計時器,意味著你允許系統拖延你的計時器來將計時器任務與其他任務聯合起來一起 執行。
總結
現在你知道怎樣使用GCD的dispatch source功能來監視文件描述符、計時器、聯結的用戶事件以及其他類似的行為。由于dispatch source完全與dispatch queue相集成,所以你可以使用任意的dispatch queue。你可以將一個dispatch source的句柄在主線程中執行、在全局隊列中并發執行、或者在用戶隊列中串行執行(執行時會將程序的其他模塊的運算考慮在內)。
下一篇我會討論如何對dispatch queue進行掛起、恢復、重定目標操作;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能。
GCD介紹(四): 完結
Dispatch Queue掛起
dispatch queue可以被掛起和恢復。使用
一個要注意的地方是,dispatch queue的掛起是block粒度的。換句話說,掛起一個queue并不會將當前正在執行的block掛起。它會允許當前執行的block執行完畢,然后后續的block不再會被執行,直至queue被恢復。
還有一個注意點:從man頁上得來的:如果你掛起了一個queue或者source,那么銷毀它之前,必須先對其進行恢復。
Dispatch Queue目標指定
所有的用戶隊列都有一個目標隊列概念。從本質上講,一個用戶隊列實際上是不執行任何任務的,但是它會將任務傳遞給它的目標隊列來執行。通常,目標隊列是默認優先級的全局隊列。
用戶隊列的目標隊列可以用函數
有一個用途,是將用戶隊列的目標定為main queue。這會導致所有提交到該用戶隊列的block在主線程中執行。這樣做來替代直接在主線程中執行代碼的好處在于,我們的用戶隊列可以單獨地被掛起 和恢復,還可以被重定目標至一個全局隊列,然后所有的block會變成在全局隊列上執行(只要你確保你的代碼離開主線程不會有問題)。
還有一個用途,是將一個用戶隊列的目標隊列指定為另一個用戶隊列。這樣做可以強制多個隊列相互協調地串行執行,這樣足以構建一組隊列,通過掛起和暫 停那個目標隊列,我們可以掛起和暫停整個組。想象這樣一個程序:它掃描一組目錄并且加載目錄中的內容。為了避免磁盤競爭,我們要確定在同一個物理磁盤上同 時只有一個文件加載任務在執行。而希望可以同時從不同的物理磁盤上讀取多個文件。要實現這個,我們要做的就是創建一個dispatch queue結構,該結構為磁盤結構的鏡像。
首先,我們會掃描系統并找到各個磁盤,為每個磁盤創建一個用戶隊列。然后掃描文件系統,并為每個文件系統創建一個用戶隊列,將這些用戶隊列的目標隊 列指向合適的磁盤用戶隊列。最后,每個目錄掃描器有自己的隊列,其目標隊列指向目錄所在的文件系統的隊列。目錄掃描器枚舉自己的目錄并為每個文件向自己的 隊列提交一個block。由于整個系統的建立方式,就使得每個物理磁盤被串行訪問,而多個物理磁盤被并行訪問。除了隊列初始化過程,我們根本不需要手動干 預什么東西。
信號量
dispatch的信號量是像其他的信號量一樣的,如果你熟悉其他多線程系統中的信號量,那么這一節的東西再好理解不過了。
信號量是一個整形值并且具有一個初始計數值,并且支持兩個操作:信號通知和等待。當一個信號量被信號通知,其計數會被增加。當一個線程在一個信號量上等待時,線程會被阻塞(如果有必要的話),直至計數器大于零,然后線程會減少這個計數。
我們使用函數
單次初始化
GCD還提供單詞初始化支持,這個與pthread中的函數
這個特性的主要用途是惰性單例初始化或者其他的線程安全數據共享。典型的單例初始化技術看起來像這樣(線程安全的):
這挺好的,但是代價比較昂貴;每次調用
使用GCD,我們可以這樣重寫上面的方法,使用函數
這個稍微比
結論
這一章,我們介紹了dispatch queue的掛起、恢復和目標重定,以及這些功能的一些用途。另外,我們還介紹了如何使用dispatch 信號量和單次初始化功能。到此,我已經完成了GCD如何運作以及如何使用的介紹。
新聞熱點
疑難解答