iOS多線程編程

此文章轉載至: http://www.hrchen.com/2013/07/multi-threading-programming-of-ios-part-3/html

感謝博主node

 

2011年WWDC時推出的神器GCD。GCD: Grand Central Dispatch,是一組用於實現併發編程的C接口。GCD是基於Objective-C的Block特性開發的,基本業務邏輯和NSOperation很像,都是將工做添加到一個隊列,由系統來負責線程的生成和調度。因爲是直接使用Block,所以比NSOperation子類使用起來更方便,大大下降了多線程開發的門檻。另外,GCD是開源的喔:libdispatchios

基本用法

首先示例:web

1
2
3
4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  [self doTask];  NSLog(@"Fisinished"); });

GCD的調用接口很是簡單,就是將Job提交至Queue中,主要的提交Job接口爲:macos

  • dispatch_sync(queue, block)同步提交job dispatch_async (queue, block) 異步提交job dispatch_after(time, queue, block) 同步延遲提交job 其中第一個參數類型是dispatch_queue_t,就是一個表示隊列的數據結構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:數組

1
2
3
dispatch_async_f(dispatch_queue_t queue,  void *context,  dispatch_function_t work);

其中第三個參數就是個函數指針,即typedef void (*dispatch_function_t)(void *);;第二個參數是傳給這個函數的參數。網絡

Dispatch Queue

要添加工做到隊列Dispatch Queue中,這個隊列能夠是串行或者並行的,並行隊列會盡量的併發執行其中的工做任務,而串行隊列每次只能運行一個工做任務。數據結構

目前GCD中有三種類型的Dispatch Queue:多線程

  • Main Queue:關聯到主線程的隊列,可使用函數dispatch_get_main_queue()得到,加到這個隊列中的工做都會分發到主線程運行。主線程只有一個,所以很明顯這個是串行隊列,每次運行一個工做。
  • Global Queue:全局隊列是併發隊列,又根據優先級細分爲高優先級、默認優先級和低優先級三種。經過dispatch_get_global_queue加上優先級參數得到這個全局隊列,例如dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
  • 自定義Queue:本身建立一個隊列,經過函數dispatch_queue_create建立,例如dispatch_queue_create("com.kiloapp.test", NULL)。第一個參數是隊列的名字,Apple建議使用反DNS型的名字命名,防止重名;第二個參數是建立的queue的類型,iOS 4.3之前只支持串行,即DISPATCH_QUEUE_SERIAL(就是NULL),iOS4.3之後也開始支持並行隊列,即參數DISPATCH_QUEUE_CONCURRENT。

因爲有這些種不一樣類型的隊列,一種常見的使用模式是:

1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  [self doHardWorkInBackground];  dispatch_async(dispatch_get_main_queue(), ^{  [self updateUI];  }); });

將一些耗時的工做添加到全局隊列,讓系統分配線程去作,工做完成後再次調用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執行結束。

Dispatch Group

GCD確實很是簡單好用,不過有些場景下仍是有點問題,例如:

1
2
3
4
5
for(id obj in array) {  [self doWorkOnItem:obj]; } [self doWorkOnArray:array];

前半部分能夠用GCD獲得處理性能的提高:

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for(id obj in array)  dispatch_async(queue, ^{  [self doWorkOnItem:obj];  }); [self doWorkOnArray:array];

問題是[self doWorkOnArray:array];原先是在所有數組各個成員的工做完成後纔會執行的,如今因爲dispatch_async是異步的,[self doWorkOnArray:array];頗有可能在各個成員的工做完成前就開始運行,這明顯不符合原先的語義。若是將dispatch_async改爲dispatch_sync能夠解決問題,可是和原來的方法同樣沒有並行處理數組,使用GCD也就沒有意義了。

針對這種狀況,GCD提供了Dispatch Group能夠將一組工做集合在一塊兒,等待這組工做完成後再繼續運行。dispatch_group_create函數能夠用來建立這個Group:

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); for(id obj in array)  dispatch_group_async(group, queue, ^{  [self doWorkOnItem:obj];  }); dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_release(group); [self doWorkOnArray:array];

方法是否是很簡單,將併發的工做用dispatch_group_async異步添加到一個Group和全局隊列中,dispatch_group_wait會等待這些工做完成後再返回,這樣你就能夠再運行[self doWorkOnArray:array];

不過有點很差的是dispatch_group_wait會阻塞當前線程,若是當前是主線程豈不是很差,有更絕的dispatch_group_notify接口:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); for(id obj in array)  dispatch_group_async(group, queue, ^{  [self doWorkOnItem:obj];  }); dispatch_group_notify(group, queue, ^{  [self doWorkOnArray:array]; }); dispatch_release(group);

dispatch_group_notify函數能夠將這個Group完成後的工做也一樣添加到隊列中(若是是須要更新UI,這個隊列也能夠是主隊列),總之這樣作就徹底不會阻塞當前線程了。

Dispatch Group還有兩個接口能夠顯式的告知group要添加block操做: dispatch_group_enter(group)和dispatch_group_leave(group),這兩個接口的調用數必須平衡,不然group就沒法知道是否是處理完全部的Block了。

Dispatch Apply

若是就是要同步的執行對數組元素的逐個操做,GCD也提供了一個簡便的dispatch_apply函數:

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply([array count], queue, ^(size_t index){  [self doWorkOnItem:obj:[array objectAtIndex:index]]; }); [self doWorkOnArray:array];

Dispatch Barrier

在使用dispatch_async異步提交時,是沒法保證這些工做的執行順序的,若是須要某些工做在某個工做完成後再執行,那麼可使用Dispatch Barrier接口來實現,barrier也有同步提交dispatch_barrier_async(queue, block)和異步提交dispatch_barrier_sync(queue, block)兩種方式。例如:

1
2
3
4
5
dispatch_async(queue, block1); dispatch_async(queue, block2); dispatch_barrier_async(queue, block3); dispatch_async(queue, block4); dispatch_async(queue, block5);

dispatch_barrier_async是異步的,調用後馬上返回,即便block3到了隊列首部,也不會馬上執行,而是等到block1和block2的並行執行完成後纔會執行block3,完成後再會並行運行block4和block5。注意這裏的queue應該是一個並行隊列,並且必須是dispatch_queue_create(label, attr)建立的自定義並行隊列,不然dispatch_barrier_async操做就失去了意義。

Dispatch Source

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的類型:

1
2
3
4
5
6
7
8
9
10
#define DISPATCH_SOURCE_TYPE_DATA_ADD #define DISPATCH_SOURCE_TYPE_DATA_OR #define DISPATCH_SOURCE_TYPE_MACH_RECV #define DISPATCH_SOURCE_TYPE_MACH_SEND #define DISPATCH_SOURCE_TYPE_PROC #define DISPATCH_SOURCE_TYPE_READ #define DISPATCH_SOURCE_TYPE_SIGNAL #define DISPATCH_SOURCE_TYPE_TIMER #define DISPATCH_SOURCE_TYPE_VNODE #define DISPATCH_SOURCE_TYPE_WRITE

第二個參數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中顯示一個進度條:

1
2
3
4
5
6
7
8
9
10
11
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(source, ^{  [progressIndicator incrementBy:dispatch_source_get_data(source)]; }); dispatch_resume(source); dispatch_apply([array count], globalQueue, ^(size_t index) {  [self doWorkOnItem:obj:[array objectAtIndex:index]];  dispatch_source_merge_data(source, 1); });

注意dispatch source建立後是處於suspend狀態的,必須使用dispatch_resume來恢復,dispatch_apply中每處理一個數組元素會調用dispatch_source_merge_data加1,那麼這個source的事件handler就能夠經過dispatch_source_get_data拿到source的數據。

Dispatch Once

dispatch_once的意思是在App整個生命週期內運行而且只容許一次,相似於pthread庫中的pthread_once)。因爲dispatch_once的調試很是困難,因此最好仍是少用,單例應該是少數值得用的地方了。

傳統咱們實現單例是這樣:

1
2
3
4
5
6
7
8
9
10
+ (id)sharedManager {  static Manager *theManager = nil;  @synchronized([Manager class])  {  if(!theManager)  theManager = [[Manager alloc] init];  }  return theManager; } 

這個的成本仍是有點高,每次訪問都會有同步鎖,使用dispatch_once能夠保證只運行一次初始化:

1
2
3
4
5
6
7
8
9
+ (id)sharedWhatever {  static dispatch_once_t pred;  static Manager *theManager = nil;  dispatch_once(&pred, ^{  theManager = [[Manager alloc] init];  });  return theManager; }

須要注意dispatch_once_t最好使用全局變量或者是static的,不然可能致使沒法肯定的行爲。

Dispatch Semaphore

和其餘多線程技術同樣,GCD也支持信號量,dispatch_semaphore_create(value)用於建立一個信號量類型dispatch_semaphore_t,參數是long類型,表示信號量的初始值;dispatch_semaphore_signal(semaphore)用於通知信號量(增長一個信號量);dispatch_semaphore_wait(semaphore, timeout)用於等待信號量(減小一個信號量),第二個參數是超時時間,若是返回值小於0,會按照前後順序等待其餘信號量的通知。

管理GCD對象

全部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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef union {  struct dispatch_object_s *_do;  struct dispatch_continuation_s *_dc;  struct dispatch_queue_s *_dq;  struct dispatch_queue_attr_s *_dqa;  struct dispatch_group_s *_dg;  struct dispatch_source_s *_ds;  struct dispatch_source_attr_s *_dsa;  struct dispatch_semaphore_s *_dsema;  struct dispatch_data_s *_ddata;  struct dispatch_io_s *_dchannel;  struct dispatch_operation_s *_doperation;  struct dispatch_fld_s *_dfld; } dispatch_object_t 

Dispatch Data 對象

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生成的緩衝區時使用。示例:

1
2
void *buffer = malloc(length); dispatch_data_t data = dispatch_data_create(buffer, length, NULL, DISPATCH_DATA_DESTRUCTOR_FREE);

若是是從NSData轉換爲dispatch_data_t:

1
2
3
4
5
nsdata = [nsdata copy]; dispatch_queue_t queue = dispatch_get_global_queue(0, 0);  return dispatch_data_create([nsdata bytes], [nsdata length], queue, ^{  [nsdata release];  });

與直接使用己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)接口,示例:

1
2
3
4
5
6
7
8
9
const void *buffer; size_t length; dispatch_data_t tmpData = dispatch_data_create_map(data, &buffer, &length); //能夠獲得dispatch_data_t的內存空間地址和字節大小 //這裏咱們能夠直接使用buffer指針對應的內存 //返回的tmpData是一個新的對應data連續內存空間的dispatch_data_t dispatch_release(tmpData);

Dispatch I/O Channel

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是在系統釋放該文件描述符時的回調。示例:

1
2
3
4
5
dispatch_io_t fileChannel = dispatch_io_create(DISPATCH_IO_STREAM, STDIN_FILENO, dispatch_get_global_queue(0, 0), ^(int error) {  if(error)  fprintf(stderr, "error from stdin: %d (%s)\n", error, strerror(error));  }); 

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:

1
2
3
4
5
6
dispatch_io_read(stdinChannel, 0, SIZE_MAX, dispatch_get_global_queue(0, 0), ^(bool done, dispatch_data_t data, int error) {  if(data)  {  dispatch_io_write(stderrChannel, 0, data, dispatch_get_global_queue(0, 0), ^(bool done, dispatch_data_t data, int error) {});  } }); 

看起來使用上還挺麻煩的,須要建立Channel才能進行讀寫,所以GCD直接提供了兩個方便異步讀寫文件描述符的接口(參數含義和channel IO的相似):

1
2
3
4
5
6
7
8
9
10
11
12
void dispatch_read(  dispatch_fd_t fd,  size_t length,  dispatch_queue_t queue,  void (^handler)(dispatch_data_t data, int error)); void dispatch_write(  dispatch_fd_t fd,  dispatch_data_t data,  dispatch_queue_t queue,  void (^handler)(dispatch_data_t data, int error)); 

總結

GCD的API按功能分爲:

  • 建立管理Queue
  • 提交Job
  • Dispatch Group
  • 管理Dispatch Object
  • 信號量Semaphore
  • 隊列屏障Barrier
  • Dispatch Source
  • Queue Context數據
  • Dispatch I/O Channel
  • Dispatch Data 對象
相關文章
相關標籤/搜索