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