###### 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 做法說明