<! Category 本文目錄 dispatch_queue_t、dispatch_block_t dispatch_sync、dispatch_async dispatch_set_target_queue、dispatch_object_t dispatch_after、dispatch_ti ...
本文目錄
- dispatch_queue_t、dispatch_block_t
- dispatch_sync、dispatch_async
- dispatch_set_target_queue、dispatch_object_t
- dispatch_after、dispatch_time_t
- dispatch_group
- dispatch_barrier
- dispatch_apply
- dispatch_suspend、dispatch_resume
- dispatch_semaphore
- dispatch_once
- dispatch_io、dispatch_data
- dispatch_source
- dispatch_get_current_queue為什麼被廢棄
dispatch_queue、dispatch_block
dispatch_block_t實際上是一個自定義block類型
typedef void (^dispatch_block_t)(void);
蘋果對於它的註釋相當有意思:
如果不使用ARC,分配在堆上或者複製到堆上的block對象一定要通過發送release消息或者調用Block_release函數來釋放。
block字面量的聲明是在棧空間上,一定要杜絕這樣的創建:
dispatch_block_t block;
if (x) {
block = ^{ printf("true\n"); };
} else {
block = ^{ printf("false\n"); };
}
block(); // unsafe!!!
這樣其實相當於
if (x) {
struct Block __tmp_1 = ...; // setup details
block = &__tmp_1;
} else {
struct Block __tmp_2 = ...; // setup details
block = &__tmp_2;
}
這個示範中,棧變數的地址脫離了它聲明時的作用域。這是一個經典的C語言bug。取而代之的應該是:block字面量必須通過Block_copy函數或者發送copy消息複製到堆上。
dispatch_queue_t是任務執行的隊列類型,它通過dispatch_queue_create創建,有兩種類型
DISPATCH_QUEUE_SERIAL(NULL): 串列隊列
DISPATCH_QUEUE_CONCURRENT: 並行隊列
串列隊列中的任務是有序執行的,並行隊列中的任務是無序執行的,具體還要看以同步方式執行還是以非同步方式執行。
兩個特殊的隊列:
main quue: dispatch_get_main_queue()獲取。
global queue: dispatch_get_global_queue(0, 0)獲取,這個函數的第一個參數是服務質量;第二個參數為保留值,始終傳0.這個隊列又叫做全局並行隊列。
這兩個隊列的特點是:
放在主隊列中的任務一定在主線程中執行。
放在全局隊列中的任務一定是在子線程中執行。(大部分情況是對的,但是dispatch_sync方法優化為:使用當前線程)
dispatch_sync dispatch_async
這部分通過官方文檔和一些任務執行順序分析來理解這兩個方法的區別,順序分析時要註意以下幾個因素:
1.環境:當前所處的線程
2.隊列:將任務添加到了哪種類型的隊列
3.執行方式:同步還是非同步
①dispatch_sync
這個方法阻塞,也就是說任務一定是添加到隊列中之後並且執行完成之後,程式才會繼續運行。
蘋果的文檔說的很清楚:Submits a block object for execution on a dispatch queue and waits until that block completes.
Submits a block to a dispatch queue for synchronous execution. Unlike dispatch_async, this function does not return until the block has finished. Calling this function and targeting the current queue results in deadlock.。因此下麵的程式無論換成什麼隊列執行結果相同:
以下環境都是在主線程中
dispatch_queue_t serialQueue = dispatch_queue_create("com.mikezh.serial.test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mikezh.concurrent.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_sync(concurrentQueue/*serialQueue globalQueue 都是一樣的*/, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任務%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
begin
任務0, <NSThread: 0x600000077f80>{number = 1, name = main}
任務1, <NSThread: 0x600000077f80>{number = 1, name = main}
任務2, <NSThread: 0x600000077f80>{number = 1, name = main}
任務3, <NSThread: 0x600000077f80>{number = 1, name = main}
任務4, <NSThread: 0x600000077f80>{number = 1, name = main}
任務5, <NSThread: 0x600000077f80>{number = 1, name = main}
任務6, <NSThread: 0x600000077f80>{number = 1, name = main}
任務7, <NSThread: 0x600000077f80>{number = 1, name = main}
任務8, <NSThread: 0x600000077f80>{number = 1, name = main}
任務9, <NSThread: 0x600000077f80>{number = 1, name = main}
end
這裡global queue執行代碼調度的線程取決於環境(可見global queue中任務一定是在子線程執行這個說法是錯誤的,這個在於dispatch_sync方法的優化:As an optimization, this function invokes the block on the current thread when possible.),例如:
dispatch_async(globalQueue, ^{
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_sync(globalQueue, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任務%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
});
begin
任務0, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務1, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務2, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務3, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務4, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務5, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務6, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務7, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務8, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任務9, <NSThread: 0x61800006f880>{number = 3, name = (null)}
end
另外,蘋果也說明瞭什麼情況下會造成死鎖:在currentqueue中調用dispatch_sync方法,並且將任務添加到currentqueue中,也就是說下麵的代碼會造成死鎖:
// 主線程、主隊列、同步執行=====>死鎖
dispatch_sync(mainQueue, ^{
});
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
});
});
③dispatch_async
向queue提交非同步執行的block並立即返回。
這個函數是向隊列提交block的基本機制。
調用這個函數總是在提交block之後立即返回並且從不等待block的調用。
目標queue會參照其他的block來決定block是串列還是並行執行。相互獨立的串列隊列參照別的串列隊列來並行處理。
參數:queue
block提交到的queue.這個queue會被系統持有直到block運行完畢。
block
提交到目標調度queue中的block。這個函數會幫你執行Block_copy和Block_release。
dispatch_async可以用來分析任務執行時要考慮的就是:block提交的順序,block開始執行的順序。
1.環境中後面的代碼不會等待block的執行
2.對於串列隊列而言,block執行的順序只能和執行的順序相同,
對於並行隊列而言,因為任務的執行是並行的,所以產生提交的block順序和執行的順序不一致的情況。
對於以下程式:
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_async(queue, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任務%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
如果queue是mainQueue
begin
end
任務0, <NSThread: 0x60000006de80>{number = 1, name = main}
任務1, <NSThread: 0x60000006de80>{number = 1, name = main}
任務2, <NSThread: 0x60000006de80>{number = 1, name = main}
任務3, <NSThread: 0x60000006de80>{number = 1, name = main}
任務4, <NSThread: 0x60000006de80>{number = 1, name = main}
任務5, <NSThread: 0x60000006de80>{number = 1, name = main}
任務6, <NSThread: 0x60000006de80>{number = 1, name = main}
任務7, <NSThread: 0x60000006de80>{number = 1, name = main}
任務8, <NSThread: 0x60000006de80>{number = 1, name = main}
任務9, <NSThread: 0x60000006de80>{number = 1, name = main}
如果是serialQueue
begin
end
任務0, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務1, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務2, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務3, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務4, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務5, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務6, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務7, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務8, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任務9, <NSThread: 0x61000026a000>{number = 3, name = (null)}
如果是globalQueue
begin
end
任務0, <NSThread: 0x60800007d6c0>{number = 3, name = (null)}
任務1, <NSThread: 0x61000007ca80>{number = 4, name = (null)}
任務4, <NSThread: 0x618000261040>{number = 6, name = (null)}
任務3, <NSThread: 0x60000007f780>{number = 5, name = (null)}
任務6, <NSThread: 0x60000007e0c0>{number = 7, name = (null)}
任務7, <NSThread: 0x61000007bfc0>{number = 8, name = (null)}
任務8, <NSThread: 0x60800007d6c0>{number = 3, name = (null)}
任務9, <NSThread: 0x60800007d540>{number = 9, name = (null)}
任務2, <NSThread: 0x60800007d8c0>{number = 10, name = (null)}
任務5, <NSThread: 0x600000262b00>{number = 11, name = (null)}
如果是concurrentQueue
begin
end
任務1, <NSThread: 0x610000073a40>{number = 4, name = (null)}
任務0, <NSThread: 0x618000065040>{number = 3, name = (null)}
任務3, <NSThread: 0x600000068f80>{number = 5, name = (null)}
任務4, <NSThread: 0x618000067bc0>{number = 6, name = (null)}
任務6, <NSThread: 0x60800006ec80>{number = 7, name = (null)}
任務7, <NSThread: 0x6180000679c0>{number = 8, name = (null)}
任務8, <NSThread: 0x61000006cdc0>{number = 9, name = (null)}
任務9, <NSThread: 0x610000073a40>{number = 4, name = (null)}
任務2, <NSThread: 0x610000073c40>{number = 10, name = (null)}
任務5, <NSThread: 0x618000064d40>{number = 11, name = (null)}
dispatch_set_target_queue、dispatch_object_t
dispatch_set_target_queue的作用是:為指定的object設置目標隊列。
目標隊列負責處理這個object。object最後執行所在的隊列取決於目標隊列。另外,修改目標隊列會影響一些object的行為:
object為Dispatch queues:
這個object,也就是這個queue會繼承目標隊列的優先順序。可以使用dispatch_get_global_queue函數獲取一個有期待的優先順序的合適的目標隊列。
如果你向串列隊列提交block,同時這個串列隊列的目標隊列是一個不同的串列隊列,這個block相對於已經提交到目標隊列中的其他block不會非同步執行,對於設置同樣目標隊列的其他隊列中的block也不會非同步執行。
Important
如果你為一個隊列修改了目標隊列,你必須小心以避免創建隊列層級的迴圈(目標環)。
object為Dispatch sources:
目標隊列為source指定了它的事件處理和取消處理的block將會提交到哪裡。
object為Dispatch I/O channels:
目標隊列為I/O channel指定了I/O操作將在哪裡執行。這會影響到I/O操作的優先順序。比如,如果channel的目標隊列優先順序設置為DISPATCH_QUEUE_PRIORITY_BACKGROUND,dispatch_io_read和dispatch_io_write函數進行的任何I/O操作都會在發生資源競爭時停止。
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"hahah--%@", [NSThread currentThread]);
});
dispatch_set_target_queue(self.timer, mainQueue); // timer在主線程上執行
dispatch_resume(self.timer);
dispatch_set_target_queue方法的第一個參數object是dispatch_object類型卻可以傳遞多種類型,這是為什麼呢?
dispatch_source_t是如何定義的
// source.h
DISPATCH_SOURCE_DECL(dispatch_source);
// object.h // 非swift環境下
DISPATCH_DECL(name);
// object.h 非swift環境下
#define DISPATCH_DECL(name) OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
// object.h 非swift環境下
#define OS_OBJECT_DECL_SUBCLASS(name, super) \
OS_OBJECT_DECL_IMPL(name, <OS_OBJECT_CLASS(super)>)
// 下麵的①②③是對這個巨集的展開
#define OS_OBJECT_DECL_IMPL(name, ...) \
OS_OBJECT_DECL_PROTOCOL(name, __VA_ARGS__) \
typedef NSObject<OS_OBJECT_CLASS(name)> \
* OS_OBJC_INDEPENDENT_CLASS name##_t
// ①
#define OS_OBJECT_DECL_PROTOCOL(name, ...) \
@protocol OS_OBJECT_CLASS(name) __VA_ARGS__ \
@end
// ②
#define OS_OBJECT_CLASS(name) OS_##name
// ③
#if __has_attribute(objc_independent_class)
#define OS_OBJC_INDEPENDENT_CLASS __attribute__((objc_independent_class))
#endif // __has_attribute(objc_independent_class)
#ifndef OS_OBJC_INDEPENDENT_CLASS
#define OS_OBJC_INDEPENDENT_CLASS
#endif
因此dispatch_source_t完全展開就是:
@protocol OS_dispatch_source <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_source>* dispatch_source_t
dispatch_io_t是如何定義的
DISPATCH_DECL(dispatch_io);
OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
// ...
因此dispatch_source_t完全展開就是:
@protocol OS_dispatch_io <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_io>* dispatch_io_t
dispatch_queue_t就是
@protocol OS_dispatch_queue <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_queue>* dispatch_queue_t
類似的還有dispatch_semaphore、dispatch_data_t、dispatch_group_t這幾個類型。
他們都是一個遵守相應協議的NSObject對象類型,這些協議的基協議OS_dispatch_object就是由dispatch_object_t聲明的:
OS_OBJECT_DECL_CLASS(dispatch_object);
#define OS_OBJECT_DECL_CLASS(name) \
OS_OBJECT_DECL(name)
#define OS_OBJECT_DECL(name, ...) \
OS_OBJECT_DECL_IMPL(name, <NSObject>)
#define OS_OBJECT_DECL_IMPL(name, ...) \
OS_OBJECT_DECL_PROTOCOL(name, __VA_ARGS__) \
typedef NSObject<OS_OBJECT_CLASS(name)> \
* OS_OBJC_INDEPENDENT_CLASS name##_t
完全展開就是:
@protocol OS_dispatch_object <NSObject>
@end
typedef NSObject<OS_dispatch_object> * dispatch_object_t
dispatch_after dispatch_time_t
dispatch_after函數會在指定的時刻將block非同步地添加到指定的隊列。
支持傳遞DISPATCH_TIME_NOW作為when參數,但是不如調用dispatch_async更優。不可以傳遞DISPATCH_TIME_FOREVER。
要註意的是:
並不是在指定的時間後執行處理,而是在指定時間追加block到隊列中,因為mainQueue在主線程的runloop中執行,所以在比如每隔1/60秒執行的RunLoop中。block最快在指定時刻執行,最慢在指定時刻+1/60秒執行,並且在main queue中有大量處理追加或主線程的處理本身有延遲時,這個時間會更長。
這個方法的第一個參數是dispatch_time_t類型,它實際上是:
typedef uint64_t dispatch_time_t;
它是對時間的一個抽象表示,0代表現在, DISPATCH_TIME_FOREVER代表著無限大,可以通過兩個函數創建
/// 根據預設時鐘創建一個時間,或者修改一個已存在的時間
/// OS X 預設時鐘基於mach_absolute_time()函數
/// 參數when:需要修改的時間,如果傳遞0,這個函數會使用mach_absolute_time()返回值。
/// 參數delta:要添加的納秒數
dispatch_time_t
dispatch_time(dispatch_time_t when, int64_t delta);
/// 使用系統時間創建一個時間類型值
/// OS X 系統時鐘基於gettimeofday(3)函數
/// 參數when:需要修改的時間或依據時間,是一個struct timespec類型指針,如果傳遞NULL,這個函數會使用gettimeofday(3)返回值。
/// 參數delta:要添加的納秒數
dispatch_time_t
dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);
dispatch_group
group對於多個任務結束後執行一些操作非常有用。Apple這樣介紹:
一組block對象提交到一個隊列中來非同步調用。group是一個管理一系列block的機制。你的程式可以根據需要同步或者非同步監控這些block。另外,group對於一些依賴其他操作完成的同步代碼非常有用。
要註意的是:group中的block可以在不同的queue中執行,每一個block中可以添加更多block到group中
group會追蹤有多少個未完成的block,GCD會持有group,直到所有相關的block全部執行完成。
舉個例子:下載A、B、C三個文件,全部下載完成之後提示
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading A ...");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading B ...");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading C ...");
});
dispatch_group_notify(group, mainQueue, ^{
NSLog(@"下載完成");
});
如果是想等待全部執行完成之後再進行其他的代碼可以使用
dispatch_group_wait(group, time)
註意這個方法是阻塞方法,它有個特點:會一直等待直到到達等待的時間 或 任務執行完成才返回。
對於它的返回值,如果返回值為0,代表group中的任務已經全部完成,非0則沒有完成。
但實際開發中有這樣的場景,比如進行一組網路請求任務,每一個任務都是非同步任務,而請求完成並將數據解析完畢我們才認為是任務的完成,而這些過程又有可能是跨線程的。這時候就要使用dispatch_group_enter和dispatch_group_leave組合,通過它們可以對group進行更細粒度的控制。這兩個函數都是線程安全的,對應著添加和移除任務,因此使用時必須要成對出現。
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
dispatch_async(globalQueue, ^{
NSLog(@"downloading A ...");
dispatch_group_leave(group);
});
});
dispatch_group_async(group, globalQueue, ^{
dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
NSLog(@"downloading B ...");
dispatch_group_leave(group);
});
});
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
dispatch_async(globalQueue, ^{
NSLog(@"downloading C ...");
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, mainQueue, ^{
NSLog(@"下載完成");
});
dispatch_barrier
我們知道在對於並行隊列,使用async方法執行任務,任務被添加到隊列中是有序的,但是執行無序。但有這麼一個場景:對於數據讀操作併發執行沒有問題,可對於寫操作來說卻要控制寫過程中不再進行讀操作,以避免數據競爭問題。類似的場景很多,大體歸結為在許多併發任務中,有1個任務在執行的時候必須保證其他的任務等待其執行完畢.諸如此類的問題可以使用dispatch_barrier_asyn函數解決。
dispatch_barrier_async
提交一個非同步執行的barrier block並立即返回。
調用這個函數總是在block被提交之後立即返回,而從不等待block的執行。當barrier block到達自定義併發隊列的隊頭時,它不會被立即執行。它會等待直到當前正在執行的lock執行完畢,到這時,barrier block才會執行。
任何在barrier block後套面提交的block也不會執行,直到barrier block執行完畢。
你指定的隊列應當是一個使用dispatch_queue_create函數自己創建的並行隊列。如果傳給這個函數一個串列隊列或
global並行隊列,這個函數會像dispatch_async函數一樣。
下圖可以很好的說明這個函數的作用
dispatch_async(concurrentQueue, ^{
NSLog(@"1");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"2");
});
dispatch_barrier_async(concurrentQueue, ^{
sleep(5);
NSLog(@"barrier");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"3");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"4");
});
可以測試列印結果:
2
1
barrier
3
4
// 1、2無序一定在之前, 3、4無序一定在之後
dispatch_barrier_sync
提交一個barrier block並等待這個block執行完畢。
提交一個barrier block到隊列用來同步執行。不同於dispatch_barrier_async,這個函數直到barrier block執行完畢才返回。目標隊列是當前隊列會發生死鎖。當barrier block到達自定義併發隊列的隊頭時,它不會被立即執行。它會等待直到當前正在執行的lock執行完畢,到這時,barrier block才會執行。任何在barrier block後套面提交的block也不會執行,直到barrier block執行完畢。
你指定的隊列應當是一個使用dispatch_queue_create函數自己創建的並行隊列。如果傳給這個函數一個串列隊列或
global並行隊列,這個函數會像dispatch_sync函數一樣。
不同於dispatch_barrier_async,系統不會持有目標隊列。因為調用這個函數是同步的,它借用了調用著的引用。而且也不會對block進行Block_copy操作。
作為優化,這個函數儘可能在當前線程調用barrier block
可以看到:最後面的優化說明和dispatch_sync方法完全一致。
這個函數和dispatch_barrier_async的區別就在於能否阻塞當前的線程。
測試:
dispatch_async(concurrentQueue, ^{
NSLog(@"1");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"2");
});
// 阻塞
dispatch_barrier_sync(concurrentQueue, ^{
sleep(5); // 如果當前環境為主線程,則界面frozen
NSLog(@"barrier sync執行");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"3");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"4");
});
dispatch_apply
為執行多個操作向隊列提交一個block,並且在返回之前等待所有的操作完成。如果目標隊列是dispatch_get_global_queue返回的並行隊列,block會被並行執行,因此它必須是可重入安全的。配合併行隊列使用這個方法對於一個迴圈的高效併發來說非常有用。
NSLog(@"begin");
dispatch_apply(5, globalQueue, ^(size_t index) {
NSLog(@"%zd", index);
});
NSLog(@"end");
結果為:
begin
0
1
2
4
3
end
可以利用這個方法高效地處理數組中的數據,不過要註意:雖然會等待所有的任務執行完成才返回,但每個任務的執行是非同步無序的。
dispatch_apply(array.count, globalQueue, ^(size_t index) {
id element = array[index];
// handler
});
dispatch_suspend、dispatch_resume
dispatch_suspend
暫停在dispatch object上的block的執行。
通過暫停一個dispatch object,你的程式可以暫時阻止與這個object有關的任何block的執行。這個暫停發生在調用方法時所有正在執行的block完成之後。調用這個函數會遞增object的暫停數,而調用dispatch_resume會少這個計數,所以你必須用一個相匹配的dispatch_resume調用來平衡每一次的dispatch_suspend調用。
一旦object被恢復,任何提交到隊列中的block或者通過dispatch source觀察的事件就會執行。
dispatch_suspend(queue)
dispatch_suspend(timer)
dispatch_resume
繼續執行dispatch object上的block。
調用這個方法會遞減暫停的隊列數或暫停的事件源對象。當計數大於0時,對象會保持暫停。當暫停數重置為0,任何提交到隊列中的block或者通過dispatch source觀察的事件會被執行。
有一個例外:每次調用dispatch_resume必須是來平衡調用dispatch_suspend的。新的通過dispatch_source_create函數返回的事件源對象有一個值為1暫停計數,因此必須在事件執行之前resume。這個方法使你的程式在事件分發之前完整地配置事件源對象。對於其他情況,都不允許比調用dispatch_suspend的次數多,那會導致負的暫停計數。
dispatch_resume(queue)
dispatch_resume(timer)
dispatch_semaphore
semaphore即信號量。 在多道程式環境下,操作系統如何實現進程之間的同步和互斥顯得極為重要。荷蘭學者Dijkstra給出了一種解決併發進程間互斥與同步關係的通用方法,即信號量機制。
信號量是一個具有非負初值的整型變數,並且有一個隊列與它關聯。信號量除初始化外,僅能通過P、V 兩個操作來訪問,這兩個操作都由原語組成,即在執行過程中不可被中斷,也就是說,當一個進程在修改某個信號量時, 沒有其他進程可同時對該信號量進行修改。P操作信號量減1,如果信號量≥0,表示可以繼續執行;如果<0就要阻塞等待,直到信號量>=0。V操作信號量加1。
信號量可以模擬現實中類似於通行證的概念,即信號量>=0可以通行,而信號量<0時則需要等待增加才可以以通行。因此信號量機制編程必涉及三個函數,創建信號量、增加信號量、減少信號量。
dispatch_semaphore_t
dispatch_semaphore_create(long value)
創建一個信號量,參數為初始值。
傳入0適用於兩個線程需要解決對於一個資源的競爭。
傳入一個大於0的值適用於管理一個有限的資源池,這個池子的大小與傳入的值相等。
當你的程式不再需要信號量時,應該調用dispatch_release來釋放它對信號量對象的引用並最終釋放記憶體。(ARC會幫助處理,因此不要手動調用dispatch_release()函數)
long
dispatch_semaphopre_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待 (減少)一個信號量.
遞減信號量計數。如果遞減之後的結果值小於0,這個方法會在返回之前一直等待一個signal信號。
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
signal或者說遞增一個信號量。
遞增信號量計數。如果之前的值小於0,這個函數會非同步地喚起一個正在dispatch_semaphore_wait函數中等待的線程。
模擬一次資源競爭的問題,在多個線程中操作同一個數據是非常常見的資源競爭的情況,這樣非常容易引起數據不一致,有時候應用會異常結束。我們使用dispatch_barrier_async可以解決這個問題,但是它是對整塊block任務的隔離,而並沒有細微到對要操作的數據這個粒度的限制。例如使用可變數組模擬多線程寫數據的情況(只是模擬寫數據過程,不考慮順序),
NSUInteger count = 10;
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:count];
for (NSUInteger i = 0; i < count; i++) {
dispatch_async(globalQueue, ^{
[mutableArray addObject:[NSNumber numberWithInteger:i]];
});
}
在多線程中更新NSMutableArray, 這段代碼異常率是極高的。可以使用信號量機制進行保證線程安全性,任何一個正在寫的操作必須要完成之後,才能進行下一個寫的操作:
NSUInteger count = 10;
self.mutableArray = [NSMutableArray arrayWithCapacity:count];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (NSUInteger i = 0; i < count; i++) {
dispatch_async(globalQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.mutableArray addObject:[NSNumber numberWithInt:i]]; // 執行到這裡說明沒有阻塞,即信號量依然>=0
dispatch_semaphore_signal(semaphore);
});
}
dispatch_once
void
dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block);
在程式的生命周期只執行block一次
這個函數對程式中的全局數據(單例)的初始化非常有用。總是在使用或測試任何通過block初始化的變數之前使用這個函數。
如果在多個線程中同時調用,這個函數會同步地等待知道block執行完成。
這個predicate參數必須指向一個保存在全局區或靜態區的變數。使用自動存儲或動態存儲變數(包括OC實例變數)的predicate結果是未知的。
static Singleton *instance;
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!instance) {
instance = [[[self class] alloc] init];
}
});
return instance;
}
dispatch_io、dispatch_data
讀取較大文件時,如果將文件分成合適的大小並使用Global Queue並行讀取的話應該會比一般的讀取速度快不少,現今的輸入輸出硬體已經可以做到一次使用多個線程更快地併列讀取了,能實現這一功能的就是dispatch_io和dispatch_data.
使用dispatch_io讀寫文件可以將1個文件固定大小分為快分別在多個線程read/write.
dispatch_async (queue, ^{/*讀取 0 ~ 8191 位元組*/});
dispatch_async (queue, ^{/*讀取 8192 ~ 16383 位元組*/});
dispatch_async (queue, ^{/*讀取 16384 ~ 24575 位元組*/});
dispatch_async (queue, ^{/*讀取 24576 ~ 36767 位元組*/});
這裡有一個可以分塊讀取,然後拼裝為NSData的方法:
void read_file(int fd, void(^completion)(NSData *data)) {
NSMutableData *data = [NSMutableData data];
dispatch_queue_t pipe_q;
dispatch_io_t pipe_channel;
pipe_q = dispatch_queue_create("PipeQ", NULL);
// pipe_q = dispatch_get_main_queue();
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
close(fd);
});
dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
NSLog(@"%@", [NSThread currentThread]);
if (err == 0) {
size_t len = dispatch_data_get_size(pipedata);
if (len > 0) {
const char *bytes = NULL;
(void)dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
[data appendBytes:bytes length:len];
}
}
if (done && completion) {
completion(data.copy);
}
});
}
使用時,傳入文件描述即可:
int fd = open("/Users/Mike/Desktop/a.txt", O_RDWR);
read_file(fd, ^(NSData *data) {
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
});
dispatch_source
GCD中除了主要的Dispatch Queue外,還有不太引人註目的Dispatch Source。它是BSD系統內核慣有功能kqueue的包裝。
kqueue是在XNU內核發生各種事件時,在應用程式編程方執行處理的技術。其CPU負荷非常小,儘量不占用資源,kqueue可以說是應用程式處理XNU內核發生的各種事件的方法中最優秀的一種。
dispatch_source使用流程大致如下:
1.通過dispatch_source_create()
函數創建一個source
2.通過dispatch_source_set_event_handler()
函數為source指定處理的block。
通過dispatch_source_set_cancel_handler()
函數為source指定取消的回調,這個回調會通過dispatch_source_cancel()
函數的調用觸發。
3.通過dispatch_resume()
函數啟動source
dispatch_source_create
創建一個新的source來管理底層的系統對象,同時自動提交handler block到GCD隊列中來響應事件。
GCD source不是可重入的。任何在source暫停時或者在handler block正在執行時接收到的事件都會合併然後在source恢復或者事件handler block返回之後分發。GCD source創建時的狀態是掛起的。在創建之後或者設置一些屬性(比如handler或者context)之後,你的程式必須調用dispatch_resume來開始這個事件的分發。
如果你的app沒有使用ARC,你應該在不再使用source的時候調用dispatch_release來釋放它
Important
事件source的創建時非同步的,所以要搞清楚被監控的系統句柄產生的競爭條件。比如,如果一個source是為一個進程創建的,同時這個進程在source創建之前就存在了,那麼任何設定的取消處理都不會被調用。
參數
type
source的類型。必須是下麵列出的source類型常量之一
type | 內容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 變數增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 變數OR |
DISPATCH_SOURCE_TYPE_MACH_RECV | Mach埠發送 |
DISPATCH_SOURCE_TYPE_MACH_SEND | Mach埠接收 |
DISPATCH_SOURCE_TYPE_PROC | 檢測到與進程相關的事件 |
DISPATCH_SOURCE_TYPE_READ | 可讀取文件映像 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收信號 |
DISPATCH_SOURCE_TYPE_TIMER | 定時器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件系統有變更 |
DISPATCH_SOURCE_TYPE_WRITE | 可寫入文件映像 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 記憶體壓力 |
handle
要監控的底層系統句柄。這個參數的具體值依賴於type參數常量。
mask
期望的事件指定的flags的掩碼。這個參數的具體值依賴於type參數常量。
queue
事件處理的block提交到的GCD隊列。
Returns
創建成功返回新創建的source,否則返回NULL。
dispatch_source_set_event_handler
為指定的source設置事件處理block。
事件處理(如果設置了)會提交到source的目標隊列來響應事件的發生。
dispatch_source_set_cancel_handler
為指定的source設置事件取消block。
取消block(如果設置了)會提交到source的目標隊列來響應dispatch_source_cancel的調用,這個響應發生在系統釋放了所有對source的底層句柄,同時,source事件處理的block已經返回了
Important
為了能安全地關閉文件描述或者銷毀Mach port, 取消的處理對文件描述或者port來說是必須的。在取消處理執行之前關閉描述和port會產生競爭條件。當source事件處理仍在運行時,如果有一個新的描述被分配了與最近關閉的描述同樣的值,事件處理可能會使用錯誤的描述讀寫數據。
dispatch_source_cancel
非同步地取消source,阻止對事件處理block的再次調用。
取消操作阻止任何對事件處理block的再次調用,但是不會打斷正在執行的事件處理。一旦事件處理block執行完畢,這個可選的取消處理會提交到目標隊列。(事件處理至少執行一次??)
取消操作會在時間處理執行完成時提交到目標隊列,意味著關閉source的句柄(文件描述或者mach port)是安全的。
這個可選的取消處理只會在系統釋放了所有的對底層系統對象(文件描述或mach prots)的引用之後才提交到目標隊列。因此,取消處理是一個關閉和釋放這些系統對象非常方便的地方。要註意的是,如果文件描述或mach port最近被被source對象追蹤,在取消操作執行之前關閉或者釋放它們是無效的.
dispatch_source_set_timer
為一個timer source設置開始時間,間隔,偏差。
你的程式可以根據需要在一個timer source對象上多次調用這個函數來重置時間間隔。
開始時間這個參數也決定了這個timer上將使用什麼時鐘。如果開始時間是DISPATCH_TIME_NOW或者是dispatch_time函數創建的,這個timer基於mach_absolute_time。否則,timer的開始時間是dispatch_walltime創建的,這個timer基於gettimeofday(3)。
偏差參數是程式時間值的微量,它以毫秒為單位,為了提升系統性能和解決耗電量,它可以大到系統將timer延遲到與其他系統活動結合使用。例如,一個每5分鐘執行一個周期性任務,而程式可能會使用一個最多30秒的leeway值。要註意的是:對所有的timer而言,即使leeway值指定為0,一些潛在問題也是正常的。
調用這個函數不會對已經取消的timer source有作用。
參數
start
timer開始的時間。查看 dispatch_time 和 dispatch_walltime 獲取更多信息。
interval
以毫秒為單位的時間間隔
leeway
系統可以延遲這個timer的時間值,以毫秒為單位。
dispatch_source_testcancel
測試指定的source是否已經被取消。
你的程式會使用這個函數來測試GCD source對象是否已經被通過調用dispatch_source_cancel的方式取消。如果dispatch_source_cancel已經調用過了,這個函數會立刻返回一個非0值,如果沒有被取消則返回0.
一些其他方法
dispatch_source_get_data
返回數據。
要在事件處理的block中調用這個函數。在外面調用會發生意想不到的結果。
返回值(unsigned long )
根據source的type的不同會有不同的返回值,共有以下幾種:
type | 返回值 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 程式定義的數據 |
DISPATCH_SOURCE_TYPE_DATA_OR | 程式定義的數據 |
DISPATCH_SOURCE_TYPE_MACH_SEND | Dispatch Source Mach Send Event Flags |
DISPATCH_SOURCE_TYPE_MACH_RECV | 不適用 |
DISPATCH_SOURCE_TYPE_PROC | Dispatch Source Process Event Flags |
DISPATCH_SOURCE_TYPE_READ | 預估可讀位元組數 |
DISPATCH_SOURCE_TYPE_SIGNAL | 上次處理執行後的分發的signal數目 |
DISPATCH_SOURCE_TYPE_TIMER | 上次處理執行之後,timer啟動後執行的次數 |
DISPATCH_SOURCE_TYPE_VNODE | Dispatch Source Vnode Event Flags |
Dispatch Source Memory Pressure Event Flags | 可用的預估緩存空間 |
dispatch_source_get_mask
返回source監控的事件的掩碼。
這個掩碼是一個事件source監控的相關事件的位掩碼。任何在這個事件掩碼里沒有指定的事件都胡被忽略,同時不會為這些事件提交事件處理block。
更詳細的信息 查看flag描述常量。
返回值
返回值根據source的type不同,會是以下flag集合中的一種:
type | 返回值 |
---|---|
DISPATCH_SOURCE_TYPE_MACH_SEND | Dispatch Source Mach Send Event Flags |
DISPATCH_SOURCE_TYPE_PROC | Dispatch Source Process Event Flags |
DISPATCH_SOURCE_TYPE_VNODE | Dispatch Source Vnode Event Flags |
dispatch_source_get_handle
返回與指定source關聯的底層系統句柄。
這個返回的句柄是一個對source監控的底層系統對象的引用。
返回值
這個返回值根據source的類型不同而不同,它回事以下句柄中的一種:
| DISPATCH_SOURCE_TYPE_MACH_SEND | mach port (mach_port_t) |
| DISPATCH_SOURCE_TYPE_MACH_RECV | mach port (mach_port_t) |
| DISPATCH_SOURCE_TYPE_PROC | process identifier (pid_t) |
| DISPATCH_SOURCE_TYPE_READ | file descriptor (int) |
| DISPATCH_SOURCE_TYPE_SIGNAL | signal number (int) |
| DISPATCH_SOURCE_TYPE_VNODE | file descriptor (int) |
| Dispatch Source Memory Pressure Event Flags | file descriptor (int) |
dispatch_source_merge_data
合併數據到GCD source,這個source的類型為DISPATCH_SOURCE_TYPE_DATA_ADD
或者DISPATCH_SOURCE_TYPE_DATA_OR
,然後將事件處理提交到目標隊列。
你的程式使用這個函數來處理DISPATCH_SOURCE_TYPE_DATA_ADD
類型或DISPATCH_SOURCE_TYPE_DATA_OR
類型的事件
參數
value
使用邏輯或、邏輯與組合的source類型。傳0沒有任何作用,也不會提交處理block。
dispatch_get_current_queue為什麼被廢棄
有兩個串列隊列
dispatch_queue_t queueA = dispatch_queue_create("com.mikezh.queueA", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueB = dispatch_queue_create("com.mikezh.queueB", DISPATCH_QUEUE_SERIAL);
下麵會發生死鎖:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
if (dispatch_get_current_queue() == queueA) {
block();
}else{
dispatch_sync(queueA, block); // 程式崩潰在這裡
}
});
});
為什麼會這裡會發生死鎖:
首先這裡的隊列嵌套關係如下所示:
我們使用
if (dispatch_get_current_queue() == queueA)
進行判斷的目的是為了防止在當前隊列是queueA的情況下,向queueA中添加同步執行的block,因為這樣會發生死鎖。但是隊列嵌套關係表明,當前所在的隊列queueB被queueA嵌套,但是糟糕的是:dispatch_get_current_queue函數只能返回最內層的隊列queueB,所以這個判斷的結果不能讓我們完成起初的目的了。
這裡已經充分說明瞭dispatch_get_current_queue的弊端:我們想獲知block執行的環境是否被某個隊列嵌套來避免死鎖,而它只是簡單地只拿到最內層的隊列的功能無法解決這個問題。
在說一些解決的方法之前,看一個上面一個功能的等價寫法(如果不理解可以查看dispatch_set_target_queue部分):
dispatch_set_target_queue(queueB, queueA);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
if (dispatch_get_current_queue() == queueA) {
block();
}else{
dispatch_sync(queueA, block); // 程式崩潰在這裡
}
});
依然是死鎖的。
下麵我們來介紹一種但是如果使用specific來判斷當前的隊列,就不會死鎖:
dispatch_set_target_queue(queueB, queueA);
static int specificKey;
CFStringRef specificValue = CFSTR("at hierarchy under queueA");
dispatch_queue_set_specific(queueA,
&specificKey,
(void*)specificValue,
(dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
CFStringRef retrievedValue = dispatch_get_specific(&specificKey);
if (retrievedValue == specificValue) {
block(); // 程式走的這條分支,而不是下麵的
} else {
dispatch_sync(queueA, block);
}
});
我們可以通過dispatch_get_specific函數準確得知在隊列層級關係中是否存在specificKey指定的值,也就是說加入根據根據指定的鍵獲取不到關聯數據,那麼系統會沿著層級體系向上查找,知道找到數據或到達根隊列位置。這裡要指出的是,層級里地位最高的那個隊列總是全局併發隊列。這個過程如下圖所示:
所以上面的代碼執行到dispatch_get_specific時會在queueA找到之前設定好的值,然後返回。這時隊列的層級關係中得知存在queueA隊列,直接調用block()
,而不再走下麵的分支dispatch_sync(queueA, block);
,從而有效避免了死鎖。