iOS 多線程相關

概念

在開始多線程之前,我們先來了解幾個比較容易混淆的概念。

線程與進程

一個進程,可以擁有一個或多個線程。

runloop與線程

http://www.lxweimin.com/p/9a46e6762fca

并發與并行

并發指多個任務交替占用CPU,并行指多個CPU同時執行多個任務。好比在火車站買票,并發是指一個窗口有多人排隊買票,并行是指多個窗口有多人排隊買票。

同步和異步

同步指在執行一個函數時,如果這個函數沒有執行完畢,那么下一個函數便不能執行。異步指在執行一個函數時,不必等到這個函數執行完畢,便可開始執行下一個函數。

四種方式

iOS目前有四種多線程方式:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

Pthreads

這個方法不必多說,大家了解一下就好,按照百度百科里的解釋:

POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標準。該標準定義了創建和操縱線程的一整套API。在類Unix操作系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為操作系統的線程。

簡單的說,這是一套在很多操作系統上都適用的多線程API,所以移植性很強(然并卵)。雖然它在iOS系統中是適用的,但它是基于c語言的框架,用起來是相當的酸爽(需要程序員自己管理線程的生命周期),下面可以來體驗一下:

- (void)threads {
    // 定義一個pthread_t類型的變量
    pthread_t thread;
    // 創建一個線程,并自動運行
    pthread_create(&thread, NULL, run, NULL);
    // 設置該線程的狀態為detached,該線程結束后會自動釋放所有資源
    pthread_detach(thread);
}
void *run(void *data) { // 新線程調用的方法,里面有需要執行的任務
    NSLog(@"%@", [NSThread currentThread]);
    return NULL;
}

打印結果:

2019-05-30 22:25:55.516777+0800 studyDemo[46180:3419195] <NSThread: 0x6000014ffac0>{number = 3, name = (null)}

NSThread

這套方案是經過蘋果封裝后的,并且完全面向對象的。所以可以直接操控線程對象,非常直觀和方便。但是它的生命周期還是需要手動管理,所以使用較少。比如[NSThread currentThread],它可以獲取當前線程類,從而可以知道當前線程的各種屬性,用于調試非常方便。下面來看代碼:

- (void)threads_NSThread {
    // 創建
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadLog) object:nil];
    // 啟動
    [thread start];

    // 創建并啟動
    [NSThread detachNewThreadSelector:@selector(threadLog) toTarget:self withObject:nil];
}
- (void)threadLog {
    NSLog(@"%@", [NSThread currentThread]);
}

打印結果:

2019-05-30 22:39:37.926337+0800 studyDemo[46421:3427904] <NSThread: 0x6000029c10c0>{number = 3, name = (null)}

GCD

同步

let queue = DispatchQueue(label: "com.ffib.blog")

queue.sync {
    for i in 0..<5 {
        print(i)
    }
}

for i in 10..<15 {
    print(i)
}

output: 
0
1
2
3
4
10
11
12
13
14

從結果可以看出隊列同步操作時,當程序在進行隊列任務時,主線程的操作并不會被執行,這是由于當程序在執行同步操作時,會阻塞線程,所以需要等待隊列任務執行完畢,程序才可以繼續執行。

異步

let queue = DispatchQueue(label: "com.ffib.blog")

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

for i in 10..<15 {
    print(i)
}

output:
10
11
12
13
14
0
1
2
3
4

從結果可以看出隊列異步操作時,當程序在執行隊列任務時,不必等待隊列任務開始執行,便可執行主線程的操作。與同步執行相比,異步隊列并不會阻塞主線程,當主線程空閑時,便可執行別的任務。

QoS 優先級

在實際開發中,我們需要對任務分類,比如UI的顯示和交互操作等,屬于優先級比較高的,有些不著急操作的,比如緩存操作、用戶習慣收集等,相對來說優先級比較低。
在GCD中,我們使用隊列和優先級劃分任務,以達到更好的用戶體驗,選擇合適的優先級,可以更好的分配CPU的資源。
GCD內采用DispatchQoS結構體,如果沒有指定QoS,會使用default。
以下等級由高到低。

public struct DispatchQoS : Equatable {

     public static let userInteractive: DispatchQoS //用戶交互級別,需要在極快時間內完成的,例如UI的顯示
     
     public static let userInitiated: DispatchQoS  //用戶發起,需要在很快時間內完成的,例如用戶的點擊事件、以及用戶的手勢
     。
     public static let `default`: DispatchQoS  //系統默認的優先級,與主線程優先級一致
     
     public static let utility: DispatchQoS   //實用級別,不需要很快完成的任務
     
     public static let background: DispatchQoS  //用戶無法感知,比較耗時的一些操作

     public static let unspecified: DispatchQoS
}

以下通過兩個例子來具體看一下優先級的使用。
相同優先級

let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)

queue1.async {
    for i in 5..<10 {
        print(i)
    }
}

queue2.async {
    for i in 0..<5 {
        print(i)
    }
}
 output:
 0
 5
 1
 6
 2
 7
 3
 8
 4
 9

從結果可見,優先級相同時,兩個隊列是交替執行的。

不同優先級

let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .default)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)

queue1.async {
    for i in 0..<5 {
        print(i)
    }
}

queue2.async {
    for i in 5..<10 {
        print(i)
    }
}

output:
0
1
2
3
4
5
6
7
8
9

從結果可見,CPU會把更多的資源優先分配給優先級高的隊列,等到CPU空閑之后才會分配資源給優先級低的隊列。

主隊列默認使用擁有最高優先級,即userInteractive,所以慎用這一優先級,否則極有可能會影響用戶體驗。
一些不需要用戶感知的操作,例如緩存等,使用utility即可

串行隊列

在創建隊列時,不指定隊列類型時,默認為串行隊列。

let queue = DispatchQueue(label: "com.ffib.blog.initiallyInactive.queue", qos: .utility)

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

queue.async {
    for i in 5..<10 {
        print(i)
    }
}

queue.async {
    for i in 10..<15 {
        print(i)
    }
}
output: 
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

從結果可見隊列執行結果,是按任務添加的順序,依次執行。

并行隊列

let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes: .concurrent)

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

queue.async {
    for i in 5..<10 {
        print(i)
    }
}

queue.async {
    for i in 10..<15 {
        print(i)
    }
}
output:
5
0
10
1
2
3
11
4
6
12
7
13
8
14
9

從結果可見,所有任務是以并行的狀態執行的。另外在設置attributes參數時,參數還有另一個枚舉值initiallyInactive,表示的任務不會自動執行,需要程序員去手動觸發(queue.activate())。如果不設置,默認是添加完任務后,自動執行。
添加initiallyInactive這一屬性帶來了,更多的靈活性,可以自由的決定執行的時機。

再來看看并行隊列如何設置這一枚舉值。

let queue = DispatchQueue.init(label: "Qos5", qos: .utility, attributes: [.concurrent, .initiallyInactive])

延遲執行

GCD提供了任務延時執行的方法,通過對已創建的隊列,調用延時任務的函數即可。其中時間以DispatchTimeInterval設置,GCD內跟時間參數有關系的參數都是通過這一枚舉來設置。

public enum DispatchTimeInterval : Equatable {

    case seconds(Int)     //秒

    case milliseconds(Int) //毫秒

    case microseconds(Int) //微妙

    case nanoseconds(Int)  //納秒

    case never
}

在設置調用函數時,asyncAfter有兩個及其相同的方法,不同的地方在于參數名有所不同,參照Stack Overflow的解釋。

wallDeadline 和 deadline,當系統睡眠后,wallDeadline會繼續,但是deadline會被掛起。例如:設置參數為60分鐘,當系統睡眠50分鐘,wallDeadline會在系統醒來之后10分鐘執行,而deadline會在系統醒來之后60分鐘執行。

let queue = DispatchQueue(label: "com.ffib.blog.after.queue")

let time = DispatchTimeInterval.seconds(5)

queue.asyncAfter(wallDeadline: .now() + time) {
    print("wall dead line done")
}

queue.asyncAfter(deadline: .now() + time) {
    print("dead line done")
}

DispatchQueue.main.asyncAfter(deadline: .now() + Double(NSEC_PER_SEC * 1) / Double(NSEC_PER_SEC)) {
    print("主線程 dead line done")
}

dispatch_group (組)

如果想等到所有的隊列的任務執行完畢再進行某些操作時,可以使用DispatchGroup來完成。

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
    for i in 0..<5 {
        print(i)
    }
}
queue2.async(group: group) {
    for i in 6..<10 {
        print(i)
    }
}

//group內所有線程的任務執行完畢
group.notify(queue: DispatchQueue.main) {
    print("done")
}

output: 
5
0
6
1
7
2
8
3
9
4
done

如果想等待某一隊列先執行完畢再執行其他隊列可以使用wait

group.wait()
// 上方的output會變為:0~9 done,因為wait會同步等待先前提交的工作完成。(類似同步執行)

為防止隊列執行任務時出現阻塞,導致線程鎖死,可以設置超時時間。

// 同步等待先前提交的工作完成,如果在指定的超時時間之前工作未完成,則返回。
group.wait(timeout: <#T##DispatchTime#>) 
// 作用同上,DispatchTime為主板時間CPU時鐘計時, DispatchWallTime為實際時間即系統時間
group.wait(wallTimeout: <#T##DispatchWallTime#>)

DispatchWorkItem

Swift3新增的api,可以通過此api設置隊列執行的任務。先看看簡單應用吧。通過DispatchWorkItem初始化閉包。

let workItem = DispatchWorkItem {
    for i in 0..<10 {
        print(i)
    }
}

調用一共分兩種情況,第一種是通過調用perform(),自動響應閉包。

 DispatchQueue.global().async {
     workItem.perform()
 }

第二種是作為參數傳給async方法。

 DispatchQueue.global().async(execute: workItem)

接下來我們來看看DispatchWorkItem的內部都有些什么方法和屬性。

init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
    block: @escaping () -> Void)

從初始化方法開始,DispatchWorkItem也可以設置優先級,另外還有個參數DispatchWorkItemFlags,來看看DispatchWorkItemFlags的內部組成。

public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {

    public static let barrier: DispatchWorkItemFlags 

    public static let detached: DispatchWorkItemFlags

    public static let assignCurrentContext: DispatchWorkItemFlags

    public static let noQoS: DispatchWorkItemFlags

    public static let inheritQoS: DispatchWorkItemFlags

    public static let enforceQoS: DispatchWorkItemFlags
}

DispatchWorkItemFlags主要分為兩部分:

  • 覆蓋
    • noQoS 沒有優先級
    • inheritQoS 繼承Queue的優先級
    • enforceQoS 覆蓋Queue的優先級
  • 執行情況
    • barrier
    • detached
    • assignCurrentContext

執行情況會在下文會具體描述,先在這留個坑。
先來看看設置優先級,會對任務執行有什么影響。

let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated) {
    for i in 0..<5 {
        print(i)
    }
}
let workItem2 = DispatchWorkItem(qos: .utility) {
    for i in 5..<10 {
        print(i)
    }
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)

output:
5
6
7
8
9
0
1
2
3
4

由結果可見即使設置了DispatchWorkItem僅僅只設置了優先級并不會對任務執行順序有任何影響。也就是仍然按照queue的優先級執行。
接下來,再來設置DispatchWorkItemFlags試試

let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)

let workItem1 = DispatchWorkItem(qos: .userInitiated, flags: .enforceQoS) {
    for i in 0..<5 {
        print(i)
    }
}

let workItem2 = DispatchWorkItem {
    for i in 5..<10 {
        print(i)
    }
}

queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
1
7
2
8
3
9
4

設置enforceQoS,使優先級強制覆蓋queue的優先級,所以兩個隊列呈交替執行狀態,變為同一優先級。
DispatchWorkItem也有waitnotify方法,和DispatchGroup用法相同。wait會等待這個workItem執行完畢。會阻塞當前線程。也可以使用cancel()提前取消任務。

// 執行結束通過notify提示主隊列
workItem.notify(queue: DispatchQueue.main) {
    print("value = ", value)
}

// wait會等待這個workItem執行完畢。會阻塞當前線程。workItem3會先執行完,之后再執行workItem2
queue12.async(execute: workItem4)
queue13.async(execute: workItem3)
workItem3.wait()

dispatch_once (單次)

一般用于單例

// swift
class Tool: NSObject {
    static let share = Tool()
}

// OC
// Tool.h
@interface Tool : NSObject
+ (instancetype)sharedInstance;
@end
// Tool.m
@implementation Tool
+ (instancetype)sharedInstance {    
    static Tool *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
@end

DispatchSemaphore(信號量)

如果你想同步執行一個異步隊列任務,可以使用信號量。
wait()會使信號量減一,如果信號量大于1則會返回.success,否則返回timeout(超時),也可以設置超時時間。

func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult

signal()會使信號量加一,返回當前信號量。

print("DispatchSemaphore: 開始")
let semaphore = DispatchSemaphore.init(value: 1)
let queue = DispatchQueue.init(label: "semaphore0", qos: .utility)

for i in 0 ..< 5 {
    print("wait: \(i)")
    if semaphore.wait(timeout: .distantFuture) == .success {
        queue.async {
            sleep(2)
            print("semaphore: \(semaphore.signal())、\(i)")
        }
    }
}
print("DispatchSemaphore: 結束")

output:
DispatchSemaphore: 開始
wait: 0
wait: 1
semaphore: 1、0
wait: 2
semaphore: 1、1
wait: 3
semaphore: 1、2
wait: 4
semaphore: 1、3
DispatchSemaphore: 結束
semaphore: 0、4

我們來看下for循環里都發生了什么。第一遍循環遇到wait時,此時信號量為1,大于0,所以if判斷為true,進行sleep和打印操作;當第二遍循環遇到wait時,發現信號量為0,此時就會鎖死線程,直到上一遍循環的操作完成,調用signal()方法,信號量加一,才會繼續執行操作,循環以上操作。

DispatchSemaphore還有另外一個用法,可以限制隊列的最大并發量,通過前面所說的wait()信號量減一,signal()信號量加一,來完成此操作,正如上文所述例子,其實達到的效果就是最大并發量為一。
如果使用過NSOperationQueue的同學,應該知道maxConcurrentOperationCount,效果是類似的。

DispatchWorkItemFlags

barrier可以理解為隔離,在讀取時,可以異步訪問,但是如果突然出現了異步寫入操作,我們想要達到的效果是在進行寫入操作的時候,使讀取操作暫停,直到寫入操作結束,再繼續進行讀取操作,以保證讀取操作獲取的是最新內容。
預期結果是:在寫入操作之前,讀取到的內容是a;在寫入操作之后,讀取到的內容是b(即寫入的內容)。
先看看不使用barrier的結果。

print("DispatchWorkItemFlags: 開始")
var testStr = "a"
let queue1 = DispatchQueue.init(label: "flags", attributes: .concurrent)
let readWorkItem = DispatchWorkItem.init {
    sleep(1)
    print(testStr)
}
let writeWorkItem = DispatchWorkItem.init {
    sleep(3)
    testStr = "b"
    print("write")
}
queue1.async(execute: readWorkItem)
queue1.async(execute: writeWorkItem)
queue1.async(execute: readWorkItem)
print("DispatchWorkItemFlags: 結束")
output:
DispatchWorkItemFlags: 開始
DispatchWorkItemFlags: 結束
a
a
write

結果不是我們想要的。再來看看加了barrier之后的效果。

// 將上題writeWorkItem修改初始化方式
let writeWorkItem = DispatchWorkItem.init(flags: [.barrier]) {
    // 里面內容同上,不變
}
output:
DispatchWorkItemFlags: 開始
DispatchWorkItemFlags: 結束
a
write
b

結果符合預期的想法,barrier主要用于讀寫隔離,以保證寫入的時候,不被讀取。

dispatch_barrier_async (柵欄)

釋義

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_barrier_async一般叫做“柵欄函數”,它就好像柵欄一樣可以將多個操作分隔開,在它前面追加的操作先執行,在它后面追加的操作后執行。
柵欄函數也可以執行隊列上的操作(參數列表中有queue和block),也有對應的 dispatch_barrier_sync 函數。

示例:

- (void)testBarrierAsync
{
    //創建一個并行隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.barrier.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    //并行操作
    void (^blk1)() = ^{
        NSLog(@"1");
    };
    void (^blk2)() = ^{
        NSLog(@"2");
    };
    void (^blk3)() = ^{
        NSLog(@"3");
    };
    void (^blk4)() = ^{
        NSLog(@"4");
    };
    void (^blk5)() = ^{
        NSLog(@"5");
    };
    void (^blk6)() = ^{
        NSLog(@"6");
    };
    
    //柵欄函數執行操作
    void (^barrierBlk)() = ^{
        NSLog(@"Barrier!");
    };
    
    //執行所有操作
    dispatch_async(concurrentQueue, blk1);
    dispatch_async(concurrentQueue, blk2);
    dispatch_async(concurrentQueue, blk3);
    dispatch_barrier_async(concurrentQueue, barrierBlk);
    dispatch_async(concurrentQueue, blk4);
    dispatch_async(concurrentQueue, blk5);
    dispatch_async(concurrentQueue, blk6);
}

方法執行結果:

 2
 1
 3
 Barrier!
 5
 4
 6

分析:
柵欄函數之前和之后的操作執行順序都不固定,但是前面三個必然先執行,然后再執行柵欄函數中的操作,最后執行后面的三個。
注意:
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block)

柵欄函數中傳入的參數隊列必須是由 dispatch_queue_create 方法創建的隊列,否則,與dispatch_async無異,起不到“柵欄”的作用了,對于dispatch_barrier_sync也是同理。

參考文獻

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本篇涵蓋多線程解析、應用等. 1.iOS多線程--徹底學會多線程之『RunLoop』2.iOS多線程--徹底學會多...
    守護地中海的花閱讀 186評論 0 3
  • iOS多線程 相關概念 1. 進程:進程(process):是指在系統中正在獨立運行的一個應用程序. 比如同時打開...
    smile麗語閱讀 315評論 0 3
  • 作為一個程序員,經常要與程序打交道。如何讓你的程序更加健壯,已經成為一個繞不開的話題,除了讓代碼本身的邏輯更加清晰...
    404ErrorCrash閱讀 302評論 0 0
  • 在 iOS 中其實目前有 4 套多線程方案,他們分別是: PthreadsNSThreadGCDNSOperati...
    春鵬閱讀 186評論 0 0
  • performSelector 需要注意的是:如果是帶afterDelay的延時函數,會在內部創建一個NSTime...
    72行代碼閱讀 944評論 0 1