文章主要記錄了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是一個抽象類,可以用來封裝一個任務,其中包含代碼邏輯和數據。因為Operation是抽象類,所以編寫代碼時不能直接使用,要使用它的子類,系統默認提供的有NSInvocationOperation(Swift中不可用)和BlockOperation。
OperationQueue
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 獲取主隊列。
復制代碼
注意:
這里的暫停和取消(包括操作的取消和隊列的取消)并不代表可以將當前的操作立即取消,而是當當前的操作執行完畢之后不再執行新的操作。
暫停和取消的區別就在于:暫停操作之后還可以恢復操作,繼續向下執行;而取消操作之后,所有的操作就清空了,無法再接著執行剩下的操作。