---
# System prepended metadata

title: 執行緒
tags: [Swift, 多執行緒, iOS]

---

###### tags: `iOS` `Swift` `多執行緒`
# 執行緒
執行緒(Thread)是 CPU 中真正執行指令的部分
也可以說是作業系統中，進行運算排程的最小單位
以現在的 CPU 架構，每個核心內至少內含一個執行緒
而一個執行緒指的是行程中一個單一順序的控制流

---
==多執行緒==
一個控制流很難快速的完成多種任務，因此多執行緒就出現了
讓任務可以分散在多個執行緒中，就像多人分工一樣
所以撰寫多執行緒，就變成成為高端工程師的必備技能

在多執行緒中，有兩個重要觀念
- Sync(同步)：提早完成任務的執行緒，要等待未完成任務的執行緒，才可繼續下個任務
- Async(異步)：執行緒各自安排任務並且各自完成，每個執行緒之間是沒有關聯的

所以希望等待縮短(即下個任務執行加快)，正是使用 Async 的原因
在 APP 中，就如同耗時的任務，應使用 Async 處理，讓 APP 可以處理其他更重要的事情

:::success
因為每個平台的架構不同，所以執行緒的操作也不同
更深入的執行緒概念，可研究一般電腦作業系統的排程
以下會針對 iOS 中的執行緒為主
:::

## APP 中的執行緒
在 Android、iOS 中，有一個非常重要的執行緒，我們稱之為 ==Main Thread(主執行緒)==
它主要用於處理手機畫面的更新，因為畫面是連續性的，所以更新的速度必須夠快
如果在 Main Thread 中，處理耗時、完成時間不可預期的任務，就會造成畫面無法更新
為了解決這種狀況，我們會使用 Async，將耗時任務移到 ==其他執行緒(背景執行緒)==
在任務結束後通知 Main Thread 更新畫面

:::info
Main Thread 通常處在閒置狀態，這樣才能隨時處理使用者指派的任務(使用者操作)
若在非 Main Thread 的執行緒上進行畫面更新，可能會造成 Exception(例外)
:::

|特性|主執行緒|背景執行緒|
|:--:|:--:|:--:|
|任務類型|畫面更新、使用者操作|網路資料、資料庫讀取、複雜演算|
|期望耗時|短|長|

## iOS 多執行緒
在 iOS 中要使用「非同步」(又稱「多線程」、「多執行緒」)，有兩種方式(其他不推薦)
GCD(Grand Central Dispatch)、NSOperationQueue
以下說明 GCD 的概念 (NSOperationQueue 待補)

==GCD==
它會自動管理執行緒的生命週期(建立、排程任務、銷毀)，對開發者來說相當方便
GCD 有兩個重要概念：

- 任務：想執行的程式，在GCD是一個閉包的程式區塊，任務分成同步、非同步兩種
- 佇列：用於存放任務，分成 Main、Global、Custom 三種，執行模式有 Serial、Concurrent

:::info
Serial(串行)
意指這個佇列裡的工作是照順序執行，一次只執行一個，當前執行完後，才執行下一個
Serial queues 適合拿來處理共享的資源，因為這樣可以確保存取是按照順序來的

Concurrent(並行)
指多個工作是同時被執行的。因此不必等上一個工作執行完，才接著執行下一個
所以每個工作會擁有各自的運行時間
:::

## 建立佇列、任務
### 佇列
==Main queue(主要佇列)==
運行在主執行緒，它是串行佇列，所有 UI 更新的任務都發生在 Main queue

使用方式
```kotlin=
let mainQueue = DispatchQueue.main //取得主要佇列
mainQueue.async {
    //執行更新畫面相關的任務
}
```

==Global queue(全域佇列)==
運行在背景執行緒，它是並行佇列，耗時任務會建議放在此佇列運行
Global queue 有四種優先级：高、預設、低、背景，在使用上不需直接指定優先級
只要申明 Quality of Service(QoS) 類型，Qos 間接決定 queue 的優先級

:::info
QoS(QoSClass) 是一種定義執行緒執行順序的 enum，若沒定義也會有 default 值
而主執行緒的優先權在最前面，再來才是子執行緒
QoSClass enum 定義的名稱執行先後順序如下：
userInteractive > userInitiated > default > utility > background > unspecified
詳細的意義可以參考本章後面的補充說明
:::

使用方式
```kotlin=
let interactQueue = DispatchQueue.global(qos: .userInteractive)
let defaultQueue = DispatchQueue.global() // 通常使用這個(不特別指定優先級)
```

==Custom queue(自定義佇列)==
Custom queue 預設是串行佇列，如果指定 attributes 為 concurrent 則為並行

使用方式
```kotlin=
//label 通常使用 Bundle Identifier 的命名方式
let serialQueue = DispatchQueue(label: "com.lab.serialQueue", qos: .utility) //串行 
let concurrentQueue = DispatchQueue(label: "com.lab.concurrentQueue", qos: .utility, attributes: .concurrent) //並行
```

### 任務

使用方式
```kotlin=
let globalQueue = DispatchQueue.global()
//非同步
globalQueue.async {
    //TODO: 執行任務
}

//同步
globalQueue.sync {
    //TODO: 執行任務
}
```

### 佇列組
佇列組可以將相同、不同佇列，組成群組統一管理
它可以通知外部，組內的任務是否都已完成，或阻塞當前線程，直到組內任務都完成
因此它適合組織非同步的佇列，因為非同步的完成時間是不可預期的

將佇列加入群組有兩種方式：添加佇列時指定群組、使用配對方法標示要加入群組的佇列


==添加佇列時指定群組==
```kotlin=
let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
    //TODO: 執行任務
}
```

==使用配對方法標示要加入群組的佇列==
```kotlin=
let group = DispatchGroup()
let queue = DispatchQueue.global()

group.enter()
queue.async {
    //TODO: 執行任務
    group.leave()
}
```

:::info
兩種方式使用場景不同。如果佇列是自己建立、引用系統，那直接使用第一種方式
如果是由系統、第三方 API 創立，無法獲取佇列的情況下，只能使用第二種方式
:::

### 佇列組完成後...?
如果要等佇列組完成後執行某些事，可以使用 notify(通知)、wait(等待)
但 wait 會阻塞執行緒，因此不推薦使用

==notify==
```kotlin=
let globalQueue = DispatchQueue.global()
let mainQueue = DispatchQueue.main
let group = DispatchGroup()
print("開始")

globalQueue.async(group: group) {
    let sleepTime = Int.random(in: 1...5)
    print("任務一 : 需要執行", sleepTime, "秒")
    sleep(UInt32(sleepTime))
    print("任務一完成")
}

globalQueue.async(group: group){
    let sleepTime = Int.random(in: 1...5)
    print("任務二 : 需要執行", sleepTime, "秒")
    sleep(UInt32(sleepTime))
    print("任務二完成")
}

//通知主佇列
group.notify(queue: mainQueue) {
    print("兩個任務都已完成")
}
print("主執行緒空閒")
```

==wait==
```kotlin=
let globalQueue = DispatchQueue.global()
let mainQueue = DispatchQueue.main
let group = DispatchGroup()
print("開始")

globalQueue.async(group: group) {
    let sleepTime = Int.random(in: 1...5)
    print("任務一 : 需要執行", sleepTime, "秒")
    sleep(UInt32(sleepTime))
    print("任務一完成")
}

globalQueue.async(group: group){
    let sleepTime = Int.random(in: 1...5)
    print("任務二 : 需要執行", sleepTime, "秒")
    sleep(UInt32(sleepTime))
    print("任務二完成")
}

//等待佇列組完成
group.wait()

print("兩個任務都已完成")
```

## 補充說明
### QoS 各級意義
==主要 QoS==
|QoS等級|工作類型|工作的執行時間|
|:--:|:--:|:--:|
|User-interactive|與用戶交互的工作，例如更新界面、執行動畫。如果工作沒有快速完成，用戶界面可能會凍結。因此任務不宜多|幾乎即時|
|User-initiated|用戶已啟動並需要立即生成的工作，例如打開已保存的文檔、用戶單擊界面中的某些操作、需要這項工作才能繼續用戶互動的任務|幾乎即時，幾秒鐘或更短時間|
|Utility|可能需要時間才能完成且不需要立即結果的工作，例如下載或導入數據。用於下載任務的進度條|需要幾秒鐘到幾分鐘|
|Background|在後台運行且用戶不可見的工作，例如同步、備份|長時間，例如幾分鐘或幾小時|

==特殊 QoS==
|QoS等級|描述|
|:--:|:--:|
|Default|此QoS的優先級落在 User-initiated 和 Utility 之間。未分配 QoS 的工作被視為此等級|
|Unspecified|缺少 QoS 信息，並提示系統應推斷出環境 QoS。如果線程使用舊 API 可能會脫離線程，則線程可能具有未指定的QoS|

## 參考文章
[法蘭克 - iOS 多執行緒說明](https://medium.com/@mikru168/ios-gcd%E5%A4%9A%E5%9F%B7%E8%A1%8C%E7%B7%92%E7%9A%84%E8%AA%AA%E6%98%8E%E8%88%87%E6%87%89%E7%94%A8-c69a68d01da1)
[iOS 中的多執行緒](https://medium.com/@crafttang/gcd%E5%92%8Coperation-operationqueue-%E7%9C%8B%E8%BF%99%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B0%B1%E5%A4%9F%E4%BA%86-f38d50521543)
[串行、並行、同步、異步](http://www.ljcoder.com/15615179983900.html)
[官方文件 - QoS](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html)

## 懶人包重點
- iOS 實現多執行緒方式：GCD (Grand Central Dispatch)、NSOperationQueue
- GCD：分成任務、佇列 (兩大概念)
  - 任務：同步、非同步
  - 佇列：有 Main、Global、Custom 三種類型，執行模式有 Serial、Concurrent
    - Main：運行在主執行緒，它是串行佇列，所有 UI 更新的任務都發生在 Main queue
    - Global：運行在背景執行緒，它是並行佇列，耗時任務建議放在此佇列，有優先級
    - Custom：預設是串行佇列，如果指定 attributes 為 concurrent 則為並行
- 佇列加入群組的兩種方式：添加佇列時指定群組、使用配對方法標示要加入群組的佇列

## 未完成內容
- NSOperationQueue 做法說明