GCD使用彙總

来源:http://www.cnblogs.com/Mike-zh/archive/2017/07/20/7213841.html
-Advertisement-
Play Games

<! Category 本文目錄 dispatch_queue_t、dispatch_block_t dispatch_sync、dispatch_async dispatch_set_target_queue、dispatch_object_t dispatch_after、dispatch_ti ...


本文目錄

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);,從而有效避免了死鎖。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近遇到了一個問題,就是在`UIWebView`的代理方法里,執行`document.title`的`js`代碼無法獲取網頁標題 ...
  • 今天來介紹一個小工具`ccache`,其可以提高`xcode`的編譯速度。說起緣由,是因為我的蘋果電腦配置比較低,而每次開發調試或測試打包都需要編譯工程,雖然項目工程代碼量不算大,但是編譯的時間還是很長,尤其是修改了頭文件或者`Archive`打包時,看著進度條像蝸牛在爬一樣,心裡都有小動物在奔騰.... ...
  • 在開發`iOS`應用程式的過程中,經常需要根據不同的需求,切換到不同的項目配置,或者打不同的包(測試環境、開發環境、生產環境等等),如果每次都是手動配置,一則比較麻煩,二則容易配置錯,那麼有沒有更好的方案來解決這個問題呢?答案是:有的。我們可以根據不同的需求,創建不同的`target`,在不同需求要... ...
  • Android精選源碼 android實現最簡潔的標簽(label/tag)選擇/展示控制項 懂得智能配色的ImageView,還能給自己設置多彩的陰影哦 NicePhoto-基於 Kotlin 開發的 一款超簡單的圖片瀏覽+設置壁紙... 你的桌面從未如此炫酷(一句代碼搞定) 仿支付寶首頁下拉刷新 ...
  • 在開發`iOS`的客戶端應用時,經常需要從伺服器下載圖片,雖然系統提供了下載工具:NSData、NSURLSession等等方法,但是考慮到圖片下載過程中,需要考慮的因素比較多,比如:非同步下載、圖片緩存、錯誤處理、編碼解碼等,以及實際需要中根據不同網路載入不同畫質的圖片等等需求,因此下載操作不是一個... ...
  • 一,效果圖。 二,代碼。 ViewController.h ViewController.m ...
  • 最近研究了一下項目的組件化,把`casa`、`bang`、`limboy`的有關組件化的博客看了一遍,學到了不少東西,對目前業界的組件化方案有了一定的瞭解。這些高質量的博客大致討論了組件化的三種方案:`url-block`、`protocol-class`(和`url-controller`類似)、... ...
  • 一、概念與總結 1、淺拷貝 淺拷貝就是對記憶體地址的複製,讓目標對象指針和源對象指向同一片記憶體空間,當記憶體銷毀的時候,指向這片記憶體的幾個指針需要重新定義才可以使用,要不然會成為野指針。 淺拷貝就是拷貝指向原來對象的指針,使原對象的引用計數+1,可以理解為創建了一個指向原對象的新指針而已,並沒有創建一個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...