文集
iOS開發之多線程(1)—— 概述
iOS開發之多線程(2)—— Thread
iOS開發之多線程(3)—— GCD
iOS開發之多線程(4)—— Operation
iOS開發之多線程(5)—— Pthreads
iOS開發之多線程(6)—— 線程安全與各種鎖
目錄
- 文集
- 版本
- 簡介
- 幾個概念
-
-
- 1. 任務(Task) 和 隊列(Queue)
- 2. 同步(sync) 和 異步(async)
- 3. 串行(Serial) 和 并發(Concurrent)
- 4. 主隊列(Main Queue) 和 全局隊列(Global Queue)
-
- GCD的基本使用
-
-
- 1. 同步執行 + 串行隊列
- 2. 同步執行 + 并發隊列
- 3. 異步執行 + 串行隊列
- 4. 異步執行 + 并發隊列
- 5. 同步執行 + 主隊列
- 6. 異步執行 + 主隊列
-
- 死鎖
版本
Xcode 11.5
Swift 5.2.2
簡介
Grand Central Dispatch, 強大的中央排程器, 額… 我們還是叫GCD吧.
幾個概念
1. 任務(Task) 和 隊列(Queue)
- 任務就是将要線上程中執行的代碼(塊), GCD中為block. 執行任務有兩種方式: 同步執行和異步執行.
dispatch_async(queue, ^{
// 任務
});
- 隊列不是線程, 把這兩者區分開會比較容易駕馭本文. 隊列是用于裝載線程任務的隊形結構, 遵循FIFO(先進先出)原則. 隊列有兩種: 串行隊列和并發隊列. 另外有兩個系統提供的特殊隊列: 主隊列(串行隊列) 和 全局隊列(并發隊列).
dispatch_queue_t queue = dispatch_queue_create("我是标簽", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 這裡的任務(代碼塊)會被添加到隊列queue中
});
2. 同步(sync) 和 異步(async)
-
同步執行會阻塞目前代碼, 不具備開啟線程的能力.
但不代表同步執行就一定在目前線程執行, 例如在其他線程同步執行到主隊列, 最終是在主線程執行的(因為主線程始終存在, 是以我們說沒有開啟新線程). 除了調用主隊列, 同步執行的任務都是在目前線程完成.
// 在其他線程同步執行到主隊列
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1 %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2 %@", [NSThread currentThread]);
});
});
-----------------------------------------------------------------------------------------------------
log:
1 <NSThread: 0x600000e78140>{number = 4, name = (null)}
2 <NSThread: 0x600000e30180>{number = 1, name = main}
-
異步執行不會阻塞目前線程, 具備開啟新線程能力.
但不是說異步執行就一定會開啟新線程, 例如異步執行到主隊列不會開啟新線程, 又例如多個異步執行到相同隊列也可能不會開啟相應數量的線程.
// 異步執行+全局隊列(并發隊列)
for (int i=0; i<8; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d %@", i, [NSThread currentThread]);
});
}
-----------------------------------------------------------------------------------------------------
log:
0 <NSThread: 0x600000b76680>{number = 5, name = (null)}
2 <NSThread: 0x600000b75700>{number = 6, name = (null)}
1 <NSThread: 0x600000b75a40>{number = 4, name = (null)}
3 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
4 <NSThread: 0x600000b76680>{number = 5, name = (null)}
5 <NSThread: 0x600000b75700>{number = 6, name = (null)}
6 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
7 <NSThread: 0x600000b75a40>{number = 4, name = (null)}
GCD會根據系統資源控制并行的數量, 是以如果任務很多, 它并不會讓所有任務同時執行.
3. 串行(Serial) 和 并發(Concurrent)
隊列有兩種: 串行隊列和并發隊列. 這兩個隊列都是遵循FIFO(先進先出)原則的.
- 串行隊列裡的任務是一個一個執行的.
-
并發隊列裡的任務是可以多個同時執行的.
雖然并發隊列也是一個一個任務取出來(FIFO原則), 但是由于取出來很快(因為可以開啟多個線程來執行任務), 我們認為這些已經取出來的任務是同步執行的.
注:
- 任務被添加到并發隊列的順序是任意的, 是以最終可能以任意順序完成, 你不會知道何時開始運作下一個任務, 或者任意時刻有多少 Block 在運作. 這些完全取決于 GCD.
- GCD 會根據系統資源控制并行的數量, 是以如果任務很多, 它并不會讓所有任務同時執行. 也就是說, 開不開啟新線程(或者說開啟多少條新線程)由GCD決定, 但會保證不會阻塞目前線程.
4. 主隊列(Main Queue) 和 全局隊列(Global Queue)
- 主隊列也是串行隊列, 但是主隊列中的所有任務都會被系統放到主線程中執行. 主線程用來處理UI相關操作, 是以不要把耗時操作放到主線程中執行(不要把耗時任務放到主隊列中).
- 全局隊列是并發隊列, 系統提供了四個枚舉優先級常量(background、low、default 以及 high). 注意, 全局隊列并不是背景線程, 隊列和線程是不同的東西, 全局隊列中的任務放到哪個線程去執行由執行方式(同步或異步)以及GCD根據資源配置設定來決定.
注: Apple 的 API 也會使用這些全局隊列, 是以你添加的任何任務都不會是這些隊列中唯一的任務.
GCD的基本使用
通過了解前面的概念, 我們知道執行方式(同步和異步)和隊列(串行和并發)組合起來有四種寫法, 但由于主隊列是特殊的隊列(主隊列中的任務都會被系統放入主線程中去執行), 是以我們還得讨論主隊列和兩種執行方式的組合. 而全局隊列也是并發隊列, 故不需要單獨拿出來讨論.
綜上, 我們将讨論以下六種寫法:
- 同步執行 + 串行隊列
- 同步執行 + 并發隊列
- 異步執行 + 串行隊列
- 異步執行 + 并發隊列
- 同步執行 + 主隊列
- 異步執行 + 主隊列
先清單總結一下吧:
串行隊列 | 并發隊列 | 主隊列 | |
---|---|---|---|
同步 | 阻塞 / 不開新線程 / 按順序 | 阻塞 / 不開新線程 / 按順序 | 阻塞 / 不開新線程(在主線程) / 按順序 |
異步 | 不阻塞 / 不開或隻開一條 / 按順序 | 不阻塞 / 至少開一條 / 亂序 | 不阻塞 / 不開新線程(在主線程) / 按順序 |
什麼時候阻塞?
隻要是同步執行肯定會阻塞; 隻要是異步執行肯定不阻塞.
什麼時候開啟新線程(不包括主線程)?
tag1: 需要同時執行兩個或兩個以上任務(block)時, 才會開啟新線程 (因為一個線程同一時間隻能執行一個任務).
!!! 前方高能, 請戴好口罩. !!!
首先, 同步執行肯定不會開啟新線程. 因為同步執行的目的是為了阻塞目前線程, 等執行完了block, 才會繼續往下執行. 也就是說, 同步執行在同一時刻隻有一個任務, 是以不會開啟新線程.
異步執行可分四種情況讨論:
總結: 請回頭看tag1.
- 目前線程對應的隊列是串行隊列(串行1), 異步執行+目前隊列(串行1)不會開啟新線程. 因為目前隻有一個隊列(串行1), 而這個隊列裡的任務是一個一個順序執行的, 沒有必要開啟新線程.
- 目前線程對應的隊列是串行隊列(串行1), 異步執行+其他串行隊列(串行2)會開啟一條新線程. 因為異步執行不阻塞串行1目前的任務, 而串行2裡面的任務需要同時執行, 是以隻能開新線程. 又因為串行2裡任務是一個個順序執行的, 是以隻會新開一個線程.
- 目前線程對應的隊列是并發隊列(并發1), 異步執行+串行隊列(串行1)會開啟一條新線程. 因為并發1目前的任務和串行1裡的任務都要同時執行, 是以需要開啟新線程. 又因為串行1是串行的, 是以隻會開一條線程.
- 目前線程對應的隊列是并發隊列(并發1), 異步執行+并發隊列(不管是不是目前隊列, 并發2)都會開啟新線程. 因為并發1目前的任務和并發2裡的任務需要同時執行, 是以隻能開啟新線程. 又因為并發2是并發的(多個任務同時執行), 是以會開啟多條線程. 線程數量由GCD根據目前資源(記憶體使用狀況, 線程池中線程數等因素)決定.
1. 同步執行 + 串行隊列
- 阻塞目前線程
- 不開啟新線程
- 任務按順序依次執行
OC
// 同步執行 + 串行隊列
- (void)syncSerial {
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncSerialQueue", DISPATCH_QUEUE_SERIAL);
for (int i=0; i<5; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
}
-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600003000240>{number = 1, name = main}
0, <NSThread: 0x600003000240>{number = 1, name = main}
1, <NSThread: 0x600003000240>{number = 1, name = main}
2, <NSThread: 0x600003000240>{number = 1, name = main}
3, <NSThread: 0x600003000240>{number = 1, name = main}
4, <NSThread: 0x600003000240>{number = 1, name = main}
end, <NSThread: 0x600003000240>{number = 1, name = main}
Swift
// 同步執行 + 串行隊列
@objc func syncSerial() {
print("start, \(Thread.current)")
let queue = DispatchQueue(label: "com.KKThreadsDemo.syncSerialQueue")
for i in 0..<5 {
queue.sync {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
2. 同步執行 + 并發隊列
- 阻塞目前線程
- 不開啟新線程
- 任務按順序執行
OC
// 同步執行 + 并發隊列
- (void)syncConcurrent {
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i=0; i<5; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
}
-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000354280>{number = 1, name = main}
0, <NSThread: 0x600000354280>{number = 1, name = main}
1, <NSThread: 0x600000354280>{number = 1, name = main}
2, <NSThread: 0x600000354280>{number = 1, name = main}
3, <NSThread: 0x600000354280>{number = 1, name = main}
4, <NSThread: 0x600000354280>{number = 1, name = main}
end, <NSThread: 0x600000354280>{number = 1, name = main}
Swift
// 同步執行 + 并發隊列
@objc func syncConcurrent() {
print("start, \(Thread.current)")
// 二選一
let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", attributes: .concurrent)
let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
for i in 0..<5 {
queue.sync {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
3. 異步執行 + 串行隊列
- 不阻塞目前線程
- 不開或隻開一條新線程
- 任務按順序執行
OC
// 異步執行 + 串行隊列
- (void)asyncSerial {
#if 1
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
for (int i=0; i<5; i++) {
dispatch_async(queue, ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
#else
/* ------ 如果添加到目前線程對應的隊列, 則不開啟新線程 ------ */
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"start, %@", [NSThread currentThread]);
for (int i=0; i<5; i++) {
dispatch_async(queue, ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
});
#endif
}
-----------------------------------------------------------------------------------------------------
log if 1:
start, <NSThread: 0x600003ad00c0>{number = 1, name = main}
end, <NSThread: 0x600003ad00c0>{number = 1, name = main}
0, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
1, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
2, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
3, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
4, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
-----------------------------------------------------------------------------------------------------
log if 0:
start, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
end, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
0, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
1, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
2, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
3, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
4, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
Swift
// 異步執行 + 串行隊列
@objc func asyncSerial() {
print("start, \(Thread.current)")
// 二選一
let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue")
let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue", attributes: .init(rawValue: 0))
for i in 0..<5 {
queue.async {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
為什麼先列印了任務0再列印end ?
因為新開了一個線程4來執行任務, 且沒有阻塞目前線程, 線程4和目前線程形成并行關系, CPU線上程間來回切換運算, 不确定是先執行那一條線程.
4. 異步執行 + 并發隊列
- 不阻塞目前線程
- 至少開啟一條新線程 (數量由GCD根據目前資源決定)
- 任務亂序執行
OC
// 異步執行 + 并發隊列
- (void)asyncConcurrent {
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i=0; i<5; i++) {
dispatch_async(queue, ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
}
-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000c1c100>{number = 1, name = main}
end, <NSThread: 0x600000c1c100>{number = 1, name = main}
0, <NSThread: 0x600001c02880>{number = 6, name = (null)}
3, <NSThread: 0x600001c023c0>{number = 3, name = (null)}
4, <NSThread: 0x600001c02880>{number = 6, name = (null)}
1, <NSThread: 0x600001c024c0>{number = 5, name = (null)}
2, <NSThread: 0x600001c75c00>{number = 4, name = (null)}
Swift
// 異步執行 + 并發隊列
@objc func asyncConcurrent() {
print("start, \(Thread.current)")
// 二選一
let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", attributes: .concurrent)
let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
for i in 0..<5 {
queue.async {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
5. 同步執行 + 主隊列
如在主線程中使用, 将會造成死鎖, 系統報錯. 關于死鎖, 詳見下小節.
在非主線程中使用:
- 阻塞目前線程
- 不開新線程, 在主線程中執行
- 任務按順序執行
OC
// 同步執行 + 主隊列
- (void)syncMain {
// 在非主線程中使用
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"start, %@", [NSThread currentThread]);
for (int i=0; i<5; i++) {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
});
}
-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x60000291a880>{number = 5, name = (null)}
0, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
1, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
2, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
3, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
4, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
end, <NSThread: 0x60000291a880>{number = 5, name = (null)}
Swift
// 同步執行 + 主隊列
@objc func syncMain() {
// 在非主線程中使用
DispatchQueue.global().async {
print("start, \(Thread.current)")
for i in 0..<5 {
DispatchQueue.main.sync {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
}
6. 異步執行 + 主隊列
- 不阻塞目前線程
- 不開新線程, 在主線程中執行
- 任務按順序執行 (主線程是串行線程)
OC
// 異步執行 + 主隊列
- (void)asyncMain {
// 可在任意線程中使用
// dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"start, %@", [NSThread currentThread]);
for (int i=0; i<5; i++) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%d, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end, %@", [NSThread currentThread]);
// });
}
-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600001f94100>{number = 1, name = main}
end, <NSThread: 0x600001f94100>{number = 1, name = main}
0, <NSThread: 0x600001f94100>{number = 1, name = main}
1, <NSThread: 0x600001f94100>{number = 1, name = main}
2, <NSThread: 0x600001f94100>{number = 1, name = main}
3, <NSThread: 0x600001f94100>{number = 1, name = main}
4, <NSThread: 0x600001f94100>{number = 1, name = main}
Swift
// 異執行 + 主隊列
@objc func asyncMain() {
// 可在任意線程中使用
DispatchQueue.global().async {
print("start, \(Thread.current)")
for i in 0..<5 {
DispatchQueue.main.async {
print("\(i), \(Thread.current)")
}
}
print("end, \(Thread.current)")
}
}
死鎖
什麼是死鎖?
網友
兩個線程卡住了, 彼此等待對方完成或執行其他操作.
蘋果
You attempted to lock a system resource that would have resulted in a deadlock.
您試圖鎖定可能導緻死鎖的系統資源。
百度
死鎖是指兩個或兩個以上的程序在執行過程中,由于競争資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都将無法推進下去。
維基
In an operating system, a deadlock occurs when a process or thread enters a waiting state because a requested system resource is held by another waiting process, which in turn is waiting for another resource held by another waiting process. If a process is unable to change its state indefinitely because the resources requested by it are being used by another waiting process, then the system is said to be in a deadlock.
在作業系統中,當程序或線程進入等待狀态時會發生死鎖,因為所請求的系統資源由另一個等待程序持有,而該等待程序又正在等待另一個等待程序持有的另一個資源。如果某個程序由于另一個程序正在使用該程序所請求的資源而無法無限期更改其狀态,則稱該系統處于死鎖狀态
以上定義, 我都不滿意.
我
在GCD中, 當線程進入等待狀态時, 該線程中的一個任務和另一個任務形成循環依賴 (即每個任務都要等待另一個任務執行完, 自己才能開始或繼續), 這種陷入僵局的無限等待狀态稱之為死鎖狀态, 也即死鎖.
🍺🍺🍺
造成死鎖的原因?
兩個任務(block)互相等待.
為什麼會等待?
舉例說明:
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// block1
dispatch_sync(queue, ^{
// block2
});
});
例子中, 在block1中同步送出了一個任務block2到隊列queue, 這時queue裡面就同時有了兩個任務: block1和block2. 一方面, block1被阻塞, 需要等block2執行完傳回後才算執行結束; 另一方面, 由于這queue是串行的, 隻能一個個任務順序執行 (前一個任務執行完了後一個才能開始), 也就是說block1不執行完block2是沒辦法從隊列中取出來開始執行的, 沒開始何談結束? 你以為談戀愛啊😂😂 就這樣, 死鎖了.
舉個反例吧, 上面例子中DISPATCH_QUEUE_SERIAL改為DISPATCH_QUEUE_CONCURRENT就不會死鎖了:
dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);
解析
queue變成了并發隊列, 并發隊列裡的任務是可以多個同時執行的, 或者說, 不用等前面的任務執行完就可以立馬取出下一個任務來執行. 例子中, 雖然此時線程隻有一個(因為同步執行), 猜測是block1被儲存了上下文, 中斷去執行block2, block2傳回後再根據上下文取出block1繼續執行.
結論: 同步送出一個任務到一個串行隊列, 并且這個隊列與執行目前代碼的隊列相同, 則一定會導緻死鎖.
以下蘋果文檔可作為佐證:
蘋果文檔
If the queue you pass as a parameter to the function is a serial queue and is the same one executing the current code, calling these functions will deadlock the queue.
事實上, 這個結論是我見到過的所有GCD中死鎖的情況了. 如果有人有不同見解, 還望不因賜教. 😁😁
幾種常見的死鎖情形 (都符合上文結論):
// 死鎖 (主線程裡調用)
- (void)deadlock {
// 死鎖1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 死鎖2
dispatch_queue_t queue2 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue2, ^{
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{
NSLog(@"1, %@", [NSThread currentThread]);
});
NSLog(@"end, %@", [NSThread currentThread]);
});
// 死鎖3
dispatch_queue_t queue3 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue3, ^{
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1, %@", [NSThread currentThread]);
dispatch_sync(queue3, ^{
NSLog(@"2, %@", [NSThread currentThread]);
});
});
NSLog(@"end, %@", [NSThread currentThread]);
});
// 死鎖4
dispatch_queue_t queue4 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue4, ^{
NSLog(@"start, %@", [NSThread currentThread]);
dispatch_barrier_sync(queue4, ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"end %@", [NSThread currentThread]);
});
}
解決辦法有二:
- 使用async
- 送出任務到另一個串行/并發隊列 (隻要不是目前正在執行的串行隊列就行)
demo
https://github.com/LittleLittleKang/KKThreadsDemo