iOS 多線程記錄(一)

前言

文章主要記錄了iOS中多線程的基礎概念及使用方法,在此作一個記錄。一是加深印象,之後本身使用時也能夠方便查找及複習,二是在本身的學習過程當中,總有大牛的文章做爲引導,但願本身也能給須要這方面知識的人一些幫助。git

關於這篇文章的Demo能夠去個人github中MultiThreadDemo查看源碼,若有不當之處,但願你們指出。程序員

GCD方面的知識點,後續會繼續更新。。。github

一、概述

1.1 準備知識

1.1.1 同步和異步

  • 同步: 必須等待當前語句執行完畢,才能夠執行下一個語句。
  • 異步: 不用等待當前語句執行完畢,就能夠執行下一個語句。

1.1.2 進程與線程

  • 進程
    • 概念:系統中正在運行的應用程序。
    • 特色:每一個進程都運行在其專用且受保護的內存空間,不一樣的進程之間相互獨立,互不干擾。
  • 線程
    • 概念:一個進程要想執行任務,必須得有線程 (每個進程至少要有一條線程) 線程是進程的基本執行單元,一個進程的全部任務都是在線程中執行的。
    • 特色:一條線程在執行任務的時候是串行(按順序執行)的。若是要讓一條線程執行多個任務,那麼只能一個一個地按順序執行這些任務。也就是說,在同一時間,一條線程只能執行一個任務

1.2 多線程基本概念及原理

  • 概念: 1個進程能夠開啓多條線程,多條線程能夠併發(同時)執行不一樣的任務。
  • 原理: 同一時間,CPU只能處理一條線程,即只有一條線程在工做多線程同時執行,實際上是CPU快速地在多條線程之間進行切換。若是CPU調度線程的速度足夠快,就會形成多線程併發執行的」假象」。

1.3 優缺點

  • 優勢api

    1. 能適當提升程序的執行效率。
    2. 能適當提升資源的利用率(CPU、內存利用率)
  • 缺點數組

    1. 開啓線程須要佔用必定的內存空間,若是開啓大量的線程,會佔用大量的內存空間,從而下降程序的性能。
    2. 線程越多,CPU在調度線程上的開銷就越大。
    3. 線程越多,程序設計就會更復雜:好比 線程間通信、多線程的數據共享等。

1.4 總結

  1. 實際上,使用多線程,因爲會開線程,必然就會消耗性能,可是卻能夠提升用戶體驗。因此,綜合考慮,在保證良好的用戶體驗的前提下,能夠適當地開線程。bash

  2. 在iOS中每一個進程啓動後都會創建一個主線程(UI線程)。因爲在iOS中除了主線程,其餘子線程是獨立於Cocoa Touch的,因此只有主線程能夠更新UI界面。iOS中多線程使用並不複雜,關鍵是如何控制好各個線程的執行順序、處理好資源競爭問題。多線程

接下來就介紹一下iOS常見的幾種多線程實現方式。併發

二、 三種多線程方案

2.1 Thread

2.1.1 介紹

  • 相對於GCD和Operation來講是較輕量級的線程開發。
  • 使用比較簡單,可是須要手動管理建立線程的生命週期、同步、異步、加鎖等問題。

2.1.2 基本使用

這裏介紹Thread的三種建立方式。下方三中建立方式中的Target類爲:app

class Receiver: NSObject {
    @objc func runThread() {
        print(Thread.current)
    }
}
複製代碼
  1. 建立實例,手動啓動
// 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()
複製代碼
  1. 類方法建立並啓動
// 建立線程後自動啓動線程
Thread.detachNewThread {
    // TODO
}

Thread.detachNewThreadSelector(#selector(Receiver.runThread), toTarget: Receiver(), with: nil)
複製代碼
  1. 隱式建立並啓動
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(操做隊列)是用來控制一系列操做對象執行的。操做對象被添加進隊列後,一直存在到操做被取消或者執行完成。隊列裏的操做對象執行的順序由操做的優先級和操做之間的依賴決定。一個應用裏能夠建立多個隊列進行操做處理。

優點
  1. 可添加完成的代碼塊,在操做完成後執行。
  2. 添加操做之間的依賴關係,方便的控制執行順序。
  3. 設定操做執行的優先級。
  4. 能夠很方便的取消一個操做的執行。
  5. 使用 KVO 觀察對操做執行狀態的更改:isExecuteing、isFinished、isCancelled。

2.2.2 基本使用

由Operation 和 OperationQueue的介紹能夠獲得使用步驟:

  1. 建立操做:先將須要執行的操做封裝到一個 Operation 對象中。
  2. 建立隊列:建立 OperationQueue 對象。
  3. 將操做加入到隊列中:將 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來實現多線程。咱們須要將建立好的操做加入到隊列中去。有兩種方法:

  1. addOperation(_ op: Operation)

將建立好的Operation或其子類的實例對象直接添加。

  1. 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 獲取主隊列。
複製代碼

注意:

  1. 這裏的暫停和取消(包括操做的取消和隊列的取消)並不表明能夠將當前的操做當即取消,而是噹噹前的操做執行完畢以後再也不執行新的操做。
  2. 暫停和取消的區別就在於:暫停操做以後還能夠恢復操做,繼續向下執行;而取消操做以後,全部的操做就清空了,沒法再接着執行剩下的操做。
相關文章
相關標籤/搜索