前言
文章主要記錄了iOS中多執行緒的基礎概念及使用方法,在此做一個記錄。一是加深印象,以後自己使用時也可以方便查詢及複習,二是在自己的學習過程中,總有大牛的文章作為引導,希望自己也能給需要這方面知識的人一些幫助。
關於這篇文章的Demo可以去我的github中MultiThreadDemo檢視原始碼,如有不當之處,希望大家指出。
GCD方面的知識點,後續會繼續更新。。。
1、概述
1.1 準備知識
1.1.1 同步和非同步
- 同步: 必須等待當前語句執行完畢,才可以執行下一個語句。
- 非同步: 不用等待當前語句執行完畢,就可以執行下一個語句。
1.1.2 程式與執行緒
- 程式
- 概念:系統中正在執行的應用程式。
- 特點:每個程式都執行在其專用且受保護的記憶體空間,不同的程式之間相互獨立,互不干擾。
- 執行緒
- 概念:一個程式要想執行任務,必須得有執行緒 (每一個程式至少要有一條執行緒) 執行緒是程式的基本執行單元,一個程式的所有任務都是線上程中執行的。
- 特點:一條執行緒在執行任務的時候是序列(按順序執行)的。如果要讓一條執行緒執行多個任務,那麼只能一個一個地按順序執行這些任務。也就是說,在同一時間,一條執行緒只能執行一個任務
1.2 多執行緒基本概念及原理
- 概念: 1個程式可以開啟多條執行緒,多條執行緒可以併發(同時)執行不同的任務。
- 原理: 同一時間,CPU只能處理一條執行緒,即只有一條執行緒在工作多執行緒同時執行,其實是CPU快速地在多條執行緒之間進行切換。如果CPU排程執行緒的速度足夠快,就會造成多執行緒併發執行的”假象”。
1.3 優缺點
-
優點
- 能適當提高程式的執行效率。
- 能適當提高資源的利用率(CPU、記憶體利用率)
-
缺點
- 開啟執行緒需要佔用一定的記憶體空間,如果開啟大量的執行緒,會佔用大量的記憶體空間,從而降低程式的效能。
- 執行緒越多,CPU在排程執行緒上的開銷就越大。
- 執行緒越多,程式設計就會更復雜:比如 執行緒間通訊、多執行緒的資料共享等。
1.4 總結
-
實際上,使用多執行緒,由於會開執行緒,必然就會消耗效能,但是卻可以提高使用者體驗。所以,綜合考慮,在保證良好的使用者體驗的前提下,可以適當地開執行緒。
-
在iOS中每個程式啟動後都會建立一個主執行緒(UI執行緒)。由於在iOS中除了主執行緒,其他子執行緒是獨立於Cocoa Touch的,所以只有主執行緒可以更新UI介面。iOS中多執行緒使用並不複雜,關鍵是如何控制好各個執行緒的執行順序、處理好資源競爭問題。
接下來就介紹一下iOS常見的幾種多執行緒實現方式。
2、 三種多執行緒方案
2.1 Thread
2.1.1 介紹
- 相對於GCD和Operation來說是較輕量級的執行緒開發。
- 使用比較簡單,但是需要手動管理建立執行緒的生命週期、同步、非同步、加鎖等問題。
2.1.2 基本使用
這裡介紹Thread的三種建立方式。下方三中建立方式中的Target類為:
class Receiver: NSObject {
@objc func runThread() {
print(Thread.current)
}
}
複製程式碼
- 建立例項,手動啟動
// 1.建立執行緒
let thread_one = Thread(target: Receiver(), selector: #selector(Receiver.runThread), object: nil)
let thread_two = Thread {
// TODO
}
// 2.啟動執行緒
thread_one.start()
thread_two.start()
複製程式碼
- 類方法建立並啟動
// 建立執行緒後自動啟動執行緒
Thread.detachNewThread {
// TODO
}
Thread.detachNewThreadSelector(#selector(Receiver.runThread), toTarget: Receiver(), with: nil)
複製程式碼
- 隱式建立並啟動
let obj = Receiver()
// 隱式建立並啟動執行緒
obj.performSelector(inBackground: #selector(obj.runThread), with: nil)
複製程式碼
2.1.3 執行緒間通訊
// 去主執行緒執行指定方法
performSelector(onMainThread: Selector, with: Any?, waitUntilDone: Bool, modes: [String]?)
// 去指定執行緒執行方法
perform(aSelector: Selector, on: Thread, with: Any?, waitUntilDone: Bool, modes: [String]?)
複製程式碼
- Any?: 需要傳遞的資料
- modes?: Runloop Mode值
2.1.4 執行緒優先順序
設定執行緒優先順序時,接收一個Double型別。
數值範圍為:0.0 ~ 1.0。
對於新建立的thread來說,Priority的值一般是 0.5。但是,因為優先順序是由系統核心決定的,並不能保證這個值會是什麼。
var threadPriority: Double { get set }
複製程式碼
2.1.5 執行緒狀態與生命週期
與執行緒狀態及生命週期相關的函式:
// - 啟動執行緒的方法,進入就緒狀態等待CPU呼叫
func start()
// - 阻塞(暫停)執行緒方法,進入阻塞狀態
class func sleep(until date: Date)
class func sleep(forTimeInterval ti: TimeInterval)
// - 取消執行緒的操作,線上程執行完當前操作後,不會再繼續執行任務
func cancel()
// - 強制停止執行緒,進入死亡狀態
class func exit()
複製程式碼
cancel():方法並不是立即取消當前執行緒,而是更改執行緒的狀態,以指示它應該退出。
exit():應該避免呼叫此方法,因為它不會讓執行緒有機會清理它在執行期間分配的任何資源。
- 新建(New): 例項化執行緒物件
- 就緒(Runnable): 向執行緒物件傳送start訊息,執行緒物件被加入可排程執行緒池等待CPU排程。
- 執行(Running): CPU負責排程可排程執行緒池中執行緒的執行。執行緒執行完成之前,狀態可能會在就緒和執行之間來回切換。就緒和執行之間的狀態變化由CPU負責,程式設計師不能干預。
- 阻塞(Blocked): 當滿足某個預定條件時,可以使用休眠或鎖,阻塞執行緒執行。sleepForTimeInterval(休眠指定時長),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖)。
- 死亡(Dead): 正常死亡,執行緒執行完畢。非正常死亡,當滿足某個條件後,線上程內部中止執行/在主執行緒中止執行緒物件
系統還定義了幾個NSNotification。若你對當前執行緒狀態的改變感興趣,可以訂閱這幾個通知:
// 當除了主執行緒外的最後一個執行緒退出時
static let NSDidBecomeSingleThreaded: NSNotification.Name
// 當執行緒接收到exit()訊息時
static let NSThreadWillExit: NSNotification.Name
// 當建立第一個除主執行緒外的子執行緒時釋出,而後再建立子執行緒時不會再發出通知。
// 通知的觀察者的通知方法在主執行緒呼叫
NSWillBecomeMultiThreaded: NSNotification.Name
複製程式碼
2.1.6 其它常用方法
// 獲取主執行緒
Thread.main
// 獲取當前執行緒
Thread.current
// 獲取當前執行緒狀態
Thread.current.isCancelled
Thread.current.isFinished
Thread.current.isFinished
複製程式碼
2.2 Operation 和 OperationQueue
2.2.1 介紹
Operation是一個抽象類,可以用來封裝一個任務,其中包含程式碼邏輯和資料。因為Operation是抽象類,所以編寫程式碼時不能直接使用,要使用它的子類,系統預設提供的有NSInvocationOperation(Swift中不可用)和BlockOperation。
OperationQueue(操作佇列)是用來控制一系列操作物件執行的。操作物件被新增進佇列後,一直存在到操作被取消或者執行完成。佇列裡的操作物件執行的順序由操作的優先順序和操作之間的依賴決定。一個應用裡可以建立多個佇列進行操作處理。
優勢
- 可新增完成的程式碼塊,在操作完成後執行。
- 新增操作之間的依賴關係,方便的控制執行順序。
- 設定操作執行的優先順序。
- 可以很方便的取消一個操作的執行。
- 使用 KVO 觀察對操作執行狀態的更改:isExecuteing、isFinished、isCancelled。
2.2.2 基本使用
由Operation 和 OperationQueue的介紹可以得到使用步驟:
- 建立操作:先將需要執行的操作封裝到一個 Operation 物件中。
- 建立佇列:建立 OperationQueue 物件。
- 將操作加入到佇列中:將 Operation 物件新增到 OperationQueue 物件中。
之後呢,系統就會自動將OperationQueue的Operation取出來,在新執行緒中執行操作。
①建立操作
- NSInvocationOperation(Swift不支援)
預設是不會開啟執行緒的,只會在當前的執行緒中執行操作,可以通過Operation和OperationQueue實現多執行緒。
// 1.建立 NSInvocationOperation 物件
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.呼叫 start 方法開始執行操作
// 不會開啟執行緒
[op start];
複製程式碼
- BlockOperation
BlockOperation 是否開啟新執行緒,取決於操作的個數。如果新增的操作的個數多,就會自動開啟新執行緒。當然開啟的執行緒數是由系統來決定的。
// 1. 建立BlockOperation物件,並封裝操作
let op = BlockOperation.init {
print("init + \(Thread.current)")
}
// 2. 呼叫 start 方法開始執行操作
op.start()
複製程式碼
- 自定義繼承自 Operation 的子類
預設情況下,Operation的子類是同步執行的,如果要建立一個能夠併發的子類,我們可能需要重寫一些方法。
- start: 所有並行的 Operations 都必須重寫這個方法,然後在你想要執行的執行緒中手動呼叫這個方法。注意:任何時候都不能呼叫父類的start方法。
- main: 在start方法中呼叫,但是注意要定義獨立的自動釋放池與別的執行緒區分開。
- isExecuting: 是否執行中,需要實現KVO通知機制。
- isFinished: 是否已完成,需要實現KVO通知機制。
- **isAsynchronous:**該方法預設返回false,表示非併發執行。併發執行需要自定義並且返回true。後面會根據這個返回值來決定是否併發。
複製程式碼
②建立佇列
OperationQueue 一共有兩種佇列:主佇列、自定義佇列。其中自定義佇列同時包含了序列、併發功能。下邊是主佇列、自定義佇列的基本建立方法和特點。
// 主佇列獲取方法
let mainQueue = OperationQueue.main
// 自定義佇列建立方法
let queue = OperationQueue()
複製程式碼
- 主佇列
- 凡是新增到主佇列中的操作,都會放到主執行緒中執行。
- 自定義佇列
- 新增到這種佇列中的操作,就會自動放到子執行緒中執行。
- 同時包含了:序列、併發功能。
③將操作加入佇列
Operation 需要配合 OperationQueue來實現多執行緒。我們需要將建立好的操作加入到佇列中去。有兩種方法:
- addOperation(_ op: Operation)
將建立好的Operation或其子類的例項物件直接新增。
- addOperation(_ block: @escaping () -> Void)
直接通過block的方式新增一個操作至佇列中。
2.2.3 序列,並行控制
OperationQueue 建立的自定義佇列同時具有序列、併發功能。它的序列功能是通過屬性 最大併發運算元—maxConcurrentOperationCount用來控制一個特定佇列中可以有多少個操作同時參與併發執行。
注意:這裡 maxConcurrentOperationCount控制的不是併發執行緒的數量,而是一個佇列中同時能併發執行的最大運算元。而且一個操作也並非只能在一個執行緒中執行。
- 最大併發運算元:maxConcurrentOperationCount
- maxConcurrentOperationCount 預設情況下為-1,表示不進行限制,可進行併發執行。
- maxConcurrentOperationCount 為1時,佇列為序列佇列。只能序列執行。
- maxConcurrentOperationCount 大於1時,佇列為併發佇列。操作併發執行,當然這個值不應超過系統限制,即使自己設定一個很大的值,系統也會自動調整為 min{自己設定的值,系統設定的預設最大值}。
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation {
sleep(1)
print("1---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
sleep(1)
print("2---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
sleep(1)
print("3---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
sleep(1)
print("4---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
-----最大併發運算元為1,輸出結果:------
1---<NSThread: 0x600001ddc200>{number = 5, name = (null)}----576945144.766482
2---<NSThread: 0x600001dd0280>{number = 6, name = (null)}----576945145.775298
3---<NSThread: 0x600001dfbd00>{number = 4, name = (null)}----576945146.775842
4---<NSThread: 0x600001dd0280>{number = 6, name = (null)}----576945147.779273
-----最大併發運算元為3,輸出結果:------
2---<NSThread: 0x6000018dc0c0>{number = 5, name = (null)}----576945253.401897
1---<NSThread: 0x6000018c5d00>{number = 7, name = (null)}----576945253.401891
3---<NSThread: 0x6000018ca540>{number = 6, name = (null)}----576945253.401913
4---<NSThread: 0x6000018dc100>{number = 8, name = (null)}----576945254.403032
複製程式碼
上方輸出的結果中,分析執行緒及輸出時間可以看出:從當最大併發運算元為1時,操作是按順序序列執行的。當最大操作併發數為3時,有3個操作是併發執行的,延遲1s後執行另一個。而開啟執行緒數量是由系統決定的,不需要我們來管理。
2.2.4 操作依賴
Operation 提供了3個介面供我們管理和檢視依賴。
// 新增依賴,使當前操作依賴於操作 op 的完成。
func addDependency(_ op: Operation)
// 移除依賴,取消當前操作對操作 op 的依賴。
func removeDependency(_ op: Operation)
// 必須在當前物件開始執行之前完成執行的操作物件陣列。
var dependencies: [Operation] { get }
複製程式碼
通過新增操作依賴,無論執行幾次,其結果都是 op2 先執行,op1 後執行。
let queue = OperationQueue()
let op1 = BlockOperation {
print("op1")
}
let op2 = BlockOperation {
print("op2")
}
op1.addDependency(op2)
queue.addOperation(op1)
queue.addOperation(op2)
----輸出結果:----
op2
op1
複製程式碼
2.2.5 執行緒優先順序
OperationQueue 提供了queuePriority(優先順序)屬性,queuePriority屬性適用於同一操作佇列中的操作,不適用於不同操作佇列中的操作。預設情況下,所有新建立的操作物件優先順序都是normal。但是我們可以通過賦值來改變當前操作在同一佇列中的執行優先順序。
// 優先順序的取值
public enum QueuePriority : Int {
case veryLow
case low
case normal // default value
case high
case veryHigh
}
複製程式碼
對於新增到佇列中的操作,首先進入準備就緒的狀態(就緒狀態取決於操作之間的依賴關係),然後進入就緒狀態的操作的開始執行順序(非結束執行順序)由操作之間相對的優先順序決定(優先順序是操作物件自身的屬性)。
理解了進入就緒狀態的操作,那麼我們就理解了queuePriority 屬性的作用物件。
- queuePriority 屬性決定了進入準備就緒狀態下的操作之間的開始執行順序。並且,優先順序不能取代依賴關係。
- 如果一個佇列中既包含高優先順序操作,又包含低優先順序操作,並且兩個操作都已經準備就緒,那麼佇列先執行高優先順序操作。
- 如果,一個佇列中既包含了準備就緒狀態的操作,又包含了未準備就緒的操作,未準備就緒的操作優先順序比準備就緒的操作優先順序高。那麼,雖然準備就緒的操作優先順序低,也會優先執行。優先順序不能取代依賴關係。如果要控制操作間的啟動順序,則必須使用依賴關係。
2.2.6 執行緒間通訊
let queue = OperationQueue()
let op = BlockOperation {
print("非同步操作 -- \(Thread.current)")
// 回到主執行緒
OperationQueue.main.addOperation({
print("回到主執行緒了 -- \(Thread.current)")
})
}
queue.addOperation(op)
-----輸出結果:-----
非同步操作 -- <NSThread: 0x60000102f540>{number = 3, name = (null)}
回到主執行緒了 -- <NSThread: 0x60000100d680>{number = 1, name = main}
複製程式碼
2.2.7 其它常用方法
- Operation 常用屬性和方法
1. 取消操作的方法
* func cancel() 可取消操作,實質是標記 isCancelled 狀態。
2. 判斷操作狀態的方法
* isFinished 判斷操作是否已經結束。
* isCancelled 判斷操作是否已經標記為取消。
* isExecuting 判斷操作是否正在在執行。
* isAsynchronous 判斷操作是否非同步執行其任務。
* isReady 判斷操作是否處於準備就緒狀態,這個值和操作的依賴關係相關。
3. 操作同步
* func waitUntilFinished() 阻塞當前執行緒,直到該操作結束。可用於執行緒執行順序的同步。
* completionBlock: (() -> Void)? 會在當前操作執行完畢時執行 completionBlock。
複製程式碼
- OperationQueue 常用屬性及方法
1. 取消/暫停/恢復操作
* func cancelAllOperations() 可以取消佇列的所有操作。
* isSuspended 判斷及設定佇列是否處於暫停狀態。true為暫停狀態,false為恢復狀態。
2. 操作同步
* func waitUntilAllOperationsAreFinished() 阻塞當前執行緒,直到佇列中的操作全部執行完畢。
3. 新增/獲取操作
* func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) 向佇列中新增運算元組,wait 標誌是否阻塞當前執行緒直到所有操作結束
* operations 當前在佇列中的運算元組(某個操作執行結束後會自動從這個陣列清除)。
* operationCount 當前佇列中的運算元。
4. 獲取佇列
* current 獲取當前佇列,如果當前執行緒不是在 OperationQueue 上執行則返回 nil。
* main 獲取主佇列。
複製程式碼
注意:
- 這裡的暫停和取消(包括操作的取消和佇列的取消)並不代表可以將當前的操作立即取消,而是噹噹前的操作執行完畢之後不再執行新的操作。
- 暫停和取消的區別就在於:暫停操作之後還可以恢復操作,繼續向下執行;而取消操作之後,所有的操作就清空了,無法再接著執行剩下的操作。