前兩部分介紹了NSThread、NSRunLoop和NSOperation,本文聊聊2011年WWDC時推出的神器GCD。GCD: Grand Central Dispatch,是一組用于實現并發編程的C接口。GCD是基于Objective-C的Block特性開發的,基本業務邏輯和NSOperation很像,都是將工作添加到一個隊列,由系統來負責線程的生成和調度。由于是直接使用Block,因此比NSOperation子類使用起來更方便,大大降低了多線程開發的門檻。另外,GCD是開源的喔:libdispatch。
首先示例:
1234 | |
GCD的調用接口非常簡單,就是將Job提交至Queue中,主要的提交Job接口為:
typedef struct dispatch_queue_s *dispatch_queue_t;;block就是表示任務的Blocktypedef void (^dispatch_block_t)( void);。dispatch_async函數是異步非阻塞的,調用后會立刻返回,工作由系統在線程池中分配線程去執行工作。 dispatch_sync和dispatch_after是阻塞式的,會一直等到添加的工作完成后才會返回。
除了添加Block到Dispatch Queue,還有添加函數到Dispatch Queue的接口,例如dispatch_async對應的有dispatch_async_f:
123 | |
其中第三個參數就是個函數指針,即typedef void (*dispatch_function_t)(void *);;第二個參數是傳給這個函數的參數。
要添加工作到隊列Dispatch Queue中,這個隊列可以是串行或者并行的,并行隊列會盡可能的并發執行其中的工作任務,而串行隊列每次只能運行一個工作任務。
目前GCD中有三種類型的Dispatch Queue:
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)dispatch_queue_create("com.kiloapp.test", NULL)。第一個參數是隊列的名字,Apple建議使用反DNS型的名字命名,防止重名;第二個參數是創建的queue的類型,iOS 4.3以前只支持串行,即DISPATCH_QUEUE_SERIAL(就是NULL),iOS4.3以后也開始支持并行隊列,即參數DISPATCH_QUEUE_CONCURRENT。由于有這些種不同類型的隊列,一種常見的使用模式是:
123456 | |
將一些耗時的工作添加到全局隊列,讓系統分配線程去做,工作完成后再次調用GCD的主線程隊列去完成UI相關的工作,這樣做就不會因為大量的非UI相關工作加重主線程負擔,從而加快UI事件響應。
其他幾個可能用到的接口有:
dispatch_get_current_queue()獲取當前隊列,一般在提交的Block中使用。在提交的Block之外調用時,如果在主線程中就返回主線程Queue;如果是在其他子線程,返回的是默認的并發隊列。
dispatch_queue_get_label(queue)獲取隊列的名字,如果你自己創建的隊列沒有設置名字,那就是返回NULL。
dispatch_set_target_queue(object, queue)設置給定對象的目標隊列。這是一個非常強大的接口,目標隊列負責處理這個GCD Object(參見下面的小節“管理GCD對象”),注意這個Object還可以是另一個隊列。例如我創建了了數個私有并發隊列,而將它們的目標隊列設置為一個串行的隊列,那么我添加到這些并發隊列的任務最終還是會被串行執行。
dispatch_main()會阻塞主線程等待主隊列Main Queue中的Block執行結束。
GCD確實非常簡單好用,不過有些場景下還是有點問題,例如:
12345 | |
前半部分可以用GCD得到處理性能的提升:
123456 | |
問題是[self doWorkOnArray:array];原先是在全部數組各個成員的工作完成后才會執行的,現在由于dispatch_async是異步的,[self doWorkOnArray:array];很有可能在各個成員的工作完成前就開始運行,這明顯不符合原先的語義。如果將dispatch_async改成dispatch_sync可以解決問題,但是和原來的方法一樣沒有并行處理數組,使用GCD也就沒有意義了。
針對這種情況,GCD提供了Dispatch Group可以將一組工作集合在一起,等待這組工作完成后再繼續運行。dispatch_group_create函數可以用來創建這個Group:
123456789 | |
方法是不是很簡單,將并發的工作用dispatch_group_async異步添加到一個Group和全局隊列中,dispatch_group_wait會等待這些工作完成后再返回,這樣你就可以再運行[self doWorkOnArray:array];。
不過有點不好的是dispatch_group_wait會阻塞當前線程,如果當前是主線程豈不是不好,有更絕的dispatch_group_notify接口:
12345678910 | |
dispatch_group_notify函數可以將這個Group完成后的工作也同樣添加到隊列中(如果是需要更新UI,這個隊列也可以是主隊列),總之這樣做就完全不會阻塞當前線程了。
Dispatch Group還有兩個接口可以顯式的告知group要添加block操作: dispatch_group_enter(group)和dispatch_group_leave(group),這兩個接口的調用數必須平衡,否則group就無法知道是不是處理完所有的Block了。
如果就是要同步的執行對數組元素的逐個操作,GCD也提供了一個簡便的dispatch_apply函數:
12345 | |
在使用dispatch_async異步提交時,是無法保證這些工作的執行順序的,如果需要某些工作在某個工作完成后再執行,那么可以使用Dispatch Barrier接口來實現,barrier也有同步提交dispatch_barrier_async(queue, block)和異步提交dispatch_barrier_sync(queue, block)兩種方式。例如:
12345 | |
dispatch_barrier_async是異步的,調用后立刻返回,即使block3到了隊列首部,也不會立刻執行,而是等到block1和block2的并行執行完成后才會執行block3,完成后再會并行運行block4和block5。注意這里的queue應該是一個并行隊列,而且必須是dispatch_queue_create(label, attr)創建的自定義并行隊列,否則dispatch_barrier_async操作就失去了意義。
Run Loop有Input Source,GCD也同樣支持一系列事件監聽和處理,GCD有一組Dispatch Source接口可以監聽底層系統對象(例如文件描述符、網絡描述符、Mach Port、Unix信號、VFS文件系統的vnode等)的事件,可以設置這些事件的處理函數,如果事件發生時,Dispatch Source就可以將事件的處理方法提交到隊列中執行。
dispatch_source_t是Dispatch Source的數據結構,使用dispatch_source_create(type, handle, mask, queue)來創建,第一個參數是source的類型:
12345678910 | |
第二個參數handle和第三個參數mask與source的類型相關,有不同的含義,第四個參數是source綁定的queue,由于篇幅問題這些含義請參考《Grand Central Dispatch (GCD) Reference》。
dispatch_source_set_event_handler(source, handler)接口可以添加source的處理方法handler,這里的handler是一個block。如果是dispatch_source_set_event_handler_f(source, handler),這里的handler就是function。
dispatch_source_cancel(source)接口可以異步取消一個source,取消后上面設置dispatch_source_set_event_handler的evnet handler就不會再執行。取消一個source時,如果之前使用dispatch_source_set_cancel_handler(source, handler)設置了一個取消時的處理block,那么這個block就會在取消source的時候提交至source關聯的queue中去執行,可以用來清理資源。
dispatch_source_get_data(source)接口用于返回source需要處理的數據,根據當初創建source類型不同有不同的含義,而且這個接口必須在event handler中調用,否則返回結果可能未定義。
dispatch_source_get_handle(source)和dispatch_source_get_mask(source)接口分布用于獲取當初創建source時的兩個參數handle和mask。
dispatch_source_merge_data(source, value)接口用于將一個value值合并到souce中,這個source的類型必須是DISPATCH_SOURCE_TYPE_DATA_ADD或者DISPATCH_SOURCE_TYPE_DATA_OR。
下面舉個source的例子,使用dispatch_source_get_data和dispatch_source_merge_data,假如我們在處理上面那個數組時要在UI中顯示一個進度條:
1234567891011 | |
注意dispatch source創建后是處于suspend狀態的,必須使用dispatch_resume來恢復,dispatch_apply中每處理一個數組元素會調用dispatch_source_merge_data加1,那么這個source的事件handler就可以通過dispatch_source_get_data拿到source的數據。
dispatch_once的意思是在App整個生命周期內運行并且只允許一次,類似于pthread庫中的pthread_once)。由于dispatch_once的調試非常困難,所以最好還是少用,單例應該是少數值得用的地方了。
傳統我們實現單例是這樣:
12345678910 | |
這個的成本還是有點高,每次訪問都會有同步鎖,使用dispatch_once可以保證只運行一次初始化:
123456789 | |
需要注意dispatch_once_t最好使用全局變量或者是static的,否則可能導致無法確定的行為。
和其他多線程技術一樣,GCD也支持信號量,dispatch_semaphore_create(value)用于創建一個信號量類型dispatch_semaphore_t,參數是long類型,表示信號量的初始值;dispatch_semaphore_signal(semaphore)用于通知信號量(增加一個信號量);dispatch_semaphore_wait(semaphore, timeout)用于等待信號量(減少一個信號量),第二個參數是超時時間,如果返回值小于0,會按照先后順序等待其他信號量的通知。
所有GCD的對象同樣是有引用計數的,如果引用計數為0就被釋放,如果你不再需要所創建的GCD對象,就可以使用dispatch_release(object)將對象的引用計數減一;同樣可以使用dispatch_retain(object)將對象的引用計數加一。注意由于全局和主線程隊列對象都不需要去dispatch_release和dispatch_retain,即使調用了也沒有作用。
dispatch_suspend(queue)可以暫停一個GCD隊列的執行,當然由于是block粒度的,如果調用dispatch_suspend時正好有隊列中block正在執行,那么這些運行的block結束后不會有其他的block再被執行;同理dispatch_resume(queue)可以恢復一個GCD隊列的運行。注意dispatch_suspend的調用數目需要和dispatch_resume數目保持平衡,因為dispatch_suspend是計數的,兩次調用dispatch_suspend會設置隊列的暫停數為2,必須再調用兩次dispatch_resume才能讓隊列重新開始執行block。
可以使用dispatch_set_context(object, context)給一個GCD對象設置一個關聯的數據,第二個參數任何一個內存地址;dispatch_set_context(object)就是獲得這個關聯數據,這樣可以方便傳遞各類上下文數據。
本小節提到的GCD對象(Dispatch Object)不單指隊列dispatch_queue_t,是指在GCD中出現的各種類型,聲明類型dispatch_object_t是個union:
1234567891011121314 | |
GCD是基于C的接口,其內部處理數據是無法直接使用Objective-C的數據類型,如果要使用數據buffer時需要自己malloc一塊內存空間來用,因此GCD提供了類似Objective-C中NSData的dispatch_data_t數據結構作為數據buffer。
dispatch_data_t的類型dispatch_data_s的指針,使用dispatch_data_create(buffer, size, queue, destructor)可以創建一個dispatch_data_t,第一個參數是保存數據的內存地址,第二個參數size是數據字節大小,第三個參數queue提交destructor block的隊列,第四個參數destructor是用于釋放data的block,默認是DISPATCH_DATA_DESTRUCTOR_DEFAULT和DISPATCH_DATA_DESTRUCTOR_FREE,后者在buffer是使用malloc生成的緩沖區時使用。示例:
12 | |
如果是從NSData轉換為dispatch_data_t:
12345 | |
與直接使用己malloc分配的連續內存空間不同,dispatch_data_t可以直接將兩塊數據用dispatch_data_create_concat(dataA, dataB)拼接起來,還可以用dispatch_data_create_subrange(data, offset, length)獲取部分dispatch_data_t。
如果反過來要訪問一個dispatch_data_t對應的內存空間,就需要使用dispatch_data_create_map(data, buffer_ptr, size_ptr)接口,示例:
123456789 | |
GCD提供的這組Dispatch I/O Channel接口用于異步處理基于文件和網絡描述符的操作,可以用于文件和網絡I/O操作。
Dispatch IO Channel對象dispatch_io_t就是對一個文件或網絡描述符的封裝,使用dispatch_io_t dispatch_io_create(type, fd, queue, cleanup_hander)接口生成一個dispatch_io_t對象。第一個參數type表示channel的類型,有DISPATCH_IO_STREAM和DISPATCH_IO_RANDOM兩種,分布表示流讀寫和隨機讀寫;第二個參數fd是要操作的文件描述符;第三個參數queue是cleanup_hander提交需要的隊列;第四個參數cleanup_hander是在系統釋放該文件描述符時的回調。示例:
12345 | |
dispatch_io_close(channel, flag)可以將生成的channel關閉,第二個參數是關閉的選項,如果使用DISPATCH_IO_STOP (0x01)就會立刻中斷當前channel的讀寫操作,關閉channel。如果使用的是0,那么會在正常讀寫結束后才會關閉channel。
During a read or write operation, the channel uses the high- and low-water mark values to determine how often to enqueue the associated handler block. It enqueues the block when the number of bytes read or written is between these two values.
在channel的讀寫操作中,channel會使用low_water和high_water值來決定讀寫了多大數據才會提交相應的數據處理block,可以dispatch_io_set_low_water(channel, low_water)和dispatch_io_set_high_water(channel, high_water)設置這兩個值。
Channel的異步讀寫操作使用接口dispatch_io_read(channel, offset, length, queue, io_handler)和dispatch_io_write(channel, offset, data, queue, io_handler)。dispatch_io_read接口參數分布表示channel,偏移量,字節大小,提交IO處理block的隊列,IO處理block;dispatch_io_write接口參數分別表示channel,偏移量,數據(dispatch_data_t),提交IO處理block的隊列,IO處理block。其中io_handler的定義為^(bool done, dispatch_data_t data, int error)()。
舉個例子,將STDIN讀到的數據寫到STDERR:
123456 | |
看起來使用上還挺麻煩的,需要創建Channel才能進行讀寫,因此GCD直接提供了兩個方便異步讀寫文件描述符的接口(參數含義和channel IO的類似):
123456789101112 | |
GCD的API按功能分為:
各組接口的詳細說明還是參考《Grand Central Dispatch (GCD) Reference》。
Grand Central Dispatch (GCD) Reference
新聞熱點
疑難解答