iOS多執行緒詳解:概念篇

jackyshan_發表於2019-03-02
iOS多執行緒詳解:概念篇

講多執行緒這個話題,就免不了先了解多執行緒相關的技術概念。本文涉及到的技術概念有CPU、程式、執行緒、同非同步、佇列等概念。
也可能講的不全或者不足的地方,後續再加以補充,最近一直使用Swift進行開發,本文所有程式碼例子都會Swift4進行演示。

CPU

CPU是什麼

引自維基百科CPU中央處理器 (英語:Central Processing Unit,縮寫:CPU),是計算機的主要裝置之一,功能主要是解釋計算機指令以及處理計算機軟體中的資料。

計算機的可程式設計性主要是指對中央處理器的程式設計。

中央處理器、內部儲存器和輸入/輸出裝置是現代電腦的三大核心部件。

1970年代以前,中央處理器由多個獨立單元構成,後來發展出由積體電路製造的中央處理器,這些高度收縮的元件就是所謂的微處理器,其中分出的中央處理器最為複雜的電路可以做成單一微小功能強大的單元。

CPU主要由運算器、控制器、暫存器三部分組成,從字面意思看就是運算就是起著運算的作用,控制器就是負責發出CPU每條指令所需要的資訊,暫存器就是儲存運算或者指令的一些臨時檔案,這樣可以保證更高的速度。
CPU有著處理指令、執行操作、控制時間、處理資料四大作用,打個比喻來說,CPU就像我們的大腦,幫我們完成各種各樣的生理活動。因此如果沒有CPU,那麼電腦就是一堆廢物,無法工作。

多核CPU與多個CPU多核

引自知乎架構可以千變萬化,面向需求、綜合考量是王道。

來,簡單舉個例子。假設現在我們要設計一臺計算機的處理器部分的架構。現在擺在我們面前的有兩種選擇,多個單核CPU和單個多核CPU。如果我們選擇多個單核CPU,那麼每一個CPU都需要有較為獨立的電路支援,有自己的Cache,而他們之間通過板上的匯流排進行通訊。假如在這樣的架構上,我們要跑一個多執行緒的程式(常見典型情況),不考慮超執行緒,那麼每一個執行緒就要跑在一個獨立的CPU上,執行緒間的所有協作都要走匯流排,而共享的資料更是有可能要在好幾個Cache裡同時存在。這樣的話,匯流排開銷相比較而言是很大的,怎麼辦?那麼多Cache,即使我們不心疼儲存能力的浪費,一致性怎麼保證?如果真正做出來,還要在主機板上佔多塊地盤,給佈局佈線帶來更大的挑戰,怎麼搞定?如果我們選擇多核單CPU,那麼我們只需要一套晶片組,一套儲存,多核之間通過晶片內部匯流排進行通訊,共享使用記憶體。

在這樣的架構上,如果我們跑一個多執行緒的程式,那麼執行緒間通訊將比上一種情形更快。如果最終實現出來,對板上空間的佔用較小,佈局佈線的壓力也較小。看起來,多核單CPU完勝嘛。可是,如果需要同時跑多個大程式怎麼辦?假設倆大程式,每一個程式都好多執行緒還幾乎用滿cache,它們分時使用CPU,那在程式間切換的時候,光指令和資料的替換就要費多大事情啊!所以呢,大部分一般我們們使用的電腦,都是單CPU多核的,比如我們配的Dell T3600,有一顆Intel Xeon E5-1650,6核,虛擬為12個邏輯核心。少部分高階人士需要更強的多工併發能力,就會搞一個多顆多核CPU的機子,Mac Pro就可以有兩顆。

一個核心同時只能處理一個執行緒,單核CPU只能實現併發,而不是並行。如果有2個執行緒,雙核CPU,那這兩個執行緒是並行的,如果有三個執行緒,那麼就還是併發的。下面會講到併發與並行的區別。

程式

程式是什麼

引自維基百科程式(英語:process),是計算機中已執行程式的實體。程式為曾經是分時系統的基本運作單位。在面向程式設計的系統(如早期的UNIX,Linux 2.4及更早的版本)中,程式是程式的基本執行實體;在面向執行緒設計的系統(如當代多數作業系統、Linux 2.6及更新的版本)中,程式本身不是基本執行單位,而是執行緒的容器。

程式本身只是指令、資料及其組織形式的描述,程式才是程式(那些指令和資料)的真正執行例項。

若干程式有可能與同一個程式相關係,且每個程式皆可以同步(循序)或非同步(平行)的方式獨立執行。現代計算機系統可在同一段時間內以程式的形式將多個程式載入到記憶體中,並藉由時間共享(或稱分時多工),以在一個處理器上表現出同時(平行性)執行的感覺。

同樣的,使用多執行緒技術(多執行緒即每一個執行緒都代表一個程式內的一個獨立執行上下文)的作業系統或計算機架構,同樣程式的平行執行緒,可在多CPU主機或網路上真正同時執行(在不同的CPU上)。

iOS系統中,一個APP的執行實體代表一個程式。一個程式有獨立的記憶體空間、系統資源、埠等。在程式中可以生成多個執行緒、這些執行緒可以共享程式中的資源。

打個比方,CPU好比是一個工廠,程式是一個車間,執行緒是車間裡面的工人。車間的空間是工人們共享的,比如許多房間是每個工人都可以進出的。這象徵一個程式的記憶體空間是共享的,每個執行緒都可以使用這些共享記憶體。

程式間通訊

蒐集了一下資料,iOS大概有8種程式間的通訊方式,可能不全,後續補充。

iOS系統是相對封閉的系統,App各自在各自的沙盒(sandbox)中執行,每個App都只能讀取iPhoneiOS系統為該應用程式程式建立的資料夾AppData下的內容,不能隨意跨越自己的沙盒去訪問別的App沙盒中的內容。
所以iOS的系統中進行App間通訊的方式也比較固定,常見的App間通訊方式以及使用場景總結如下。

  • 1、Port (local socket)

上層封裝為NSMachPort : Foundation
中層封裝為CFMachPortCore Foundation
下層封裝為Mach Ports : Mach核心層(執行緒、程式都可使用它進行通訊)

一個App1在本地的埠port1234進行TCPbindlisten,另外一個App2在同一個埠port1234發起TCPconnect連線,這樣就可以建立正常的TCP連線,進行TCP通訊了,那麼就想傳什麼資料就可以傳什麼資料了。但是有一個限制,就是要求兩個App程式都在活躍狀態,而沒有被後臺殺死。尷尬的一點是iOS系統會給每個TCP在後臺600秒的網路通訊時間,600秒後APP會進入休眠狀態。

  • 2、URL Scheme

這個是iOS App通訊最常用到的通訊方式,App1通過openURL的方法跳轉到App2,並且在URL中帶上想要的引數,有點類似httpget請求那樣進行引數傳遞。這種方式是使用最多的最常見的,使用方法也很簡單隻需要源App1info.plist中配置LSApplicationQueriesSchemes,指定目標App2scheme;然後在目標App2info.plist中配置好URL types,表示該App接受何種URL Scheme的喚起。

iOS多執行緒詳解:概念篇

典型的使用場景就是各開放平臺SDK的分享功能,如分享到微信朋友圈微博等,或者是支付場景。比如從滴滴叫車結束行程跳轉到微信進行支付。

iOS多執行緒詳解:概念篇
  • 3、Keychain

iOS系統的Keychain是一個安全的儲存容器,它本質上就是一個sqllite資料庫,它的位置儲存在/private/var/Keychains/keychain-2.db,不過它所儲存的所有資料都是經過加密的,可以用來為不同的App儲存敏感資訊,比如使用者名稱,密碼等。iOS系統自己也用Keychain來儲存VPN憑證和Wi-Fi密碼。它是獨立於每個App的沙盒之外的,所以即使App被刪除之後,Keychain裡面的資訊依然存在。

基於安全和獨立於App沙盒的兩個特性,Keychain主要用於給App儲存登入和身份憑證等敏感資訊,這樣只要使用者登入過,即使使用者刪除了App重新安裝也不需要重新登入。

Keychain用於App間通訊的一個典型場景也和App的登入相關,就是統一賬戶登入平臺。使用同一個賬號平臺的多個App,只要其中一個App使用者進行了登入,其他app就可以實現自動登入不需要使用者多次輸入賬號和密碼。一般開放平臺都會提供登入SDK,在這個SDK內部就可以把登入相關的資訊都寫到Keychain中,這樣如果多個App都整合了這個SDK,那麼就可以實現統一賬戶登入了。

Keychain的使用比較簡單,使用iOS系統提供的類KeychainItemWrapper,並通過Keychain access groups就可以在應用之間共享Keychain中的資料的資料了。

import Security
// MARK: - 儲存和讀取UUID

class func saveUUIDToKeyChain() {
    var keychainItem = KeychainItemWrapper(account: "Identfier", service: "AppName", accessGroup: nil)
    var string = (keychainItem[(kSecAttrGeneric as! Any)] as! String)
    if (string == "") || !string {
        keychainItem[(kSecAttrGeneric as! Any)] = self.getUUIDString()
    }
}

class func readUUIDFromKeyChain() -> String {
    var keychainItemm = KeychainItemWrapper(account: "Identfier", service: "AppName", accessGroup: nil)
    var UUID = (keychainItemm[(kSecAttrGeneric as! Any)] as! String)
    return UUID
}

class func getUUIDString() -> String {
    var uuidRef = CFUUIDCreate(kCFAllocatorDefault)
    var strRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef)
    var uuidString = (strRef as! String).replacingOccurrencesOf("-", withString: "")
    CFRelease(strRef)
    CFRelease(uuidRef)
    return uuidString
}

複製程式碼
  • 4、UIPasteboard

顧名思義, UIPasteboard是剪下板功能,因為iOS的原生控制元件UITextViewUITextFieldUIWebView,我們在使用時如果長按,就會出現複製、剪下、選中、全選、貼上等功能,這個就是利用了系統剪下板功能來實現的。而每一個App都可以去訪問系統剪下板,所以就能夠通過系統剪貼簿進行App間的資料傳輸了。

//建立系統剪貼簿
let pasteboard = UIPasteboard.general
//往剪貼簿寫入淘口令
pasteboard.string = "複製這條資訊¥rkUy0Mz97CV¥後開啟?手淘?"

//淘寶從後臺切到前臺,讀取淘口令進行展示
let alert = UIAlertView.init(title: "淘口令", message: "發現一個寶貝,口令是rkUy0Mz97CV", delegate: self, cancelButtonTitle: "取消", otherButtonTitles: "檢視")
alert.show()

複製程式碼

UIPasteboard典型的使用場景就是淘寶跟微信/QQ的連結分享。由於騰訊和阿里的公司戰略,騰訊在微信和QQ中都遮蔽了淘寶的連結。那如果淘寶使用者想通過QQ或者微信跟好友分享某個淘寶商品,怎麼辦呢? 阿里的工程師就巧妙的利用剪貼簿實現了這個功能。首先淘寶App中將連結自定義成淘口令,引導使用者進行復制,並去QQ好友對話中貼上。然後QQ好友收到訊息後再開啟自己的淘寶App,淘寶App每次從後臺切到前臺時,就會檢查系統剪下板中是否有淘口令,如果有淘口令就進行解析並跳轉到對於的商品頁面。

iOS多執行緒詳解:概念篇

微信好友把淘口令複製到淘寶中,就可以開啟好友分享的淘寶連結了。

  • 5、UIDocumentInteractionController

UIDocumentInteractionController主要是用來實現同裝置上App之間的共享文件,以及文件預覽、列印、發郵件和複製等功能。它的使用非常簡單.

首先通過呼叫它唯一的類方法interactionControllerWithURL:,並傳入一個URL(NSURL),為你想要共享的檔案來初始化一個例項物件。然後UIDocumentInteractionControllerDelegate,然後顯示選單和預覽視窗。

let url = Bundle.main.url(forResource: "test", withExtension: "pdf")
if url != nil {
    let documentInteractionController = UIDocumentInteractionController.init(url: url!)
    documentInteractionController.delegate = self
    documentInteractionController.presentOpenInMenu(from: self.view.bounds, in: self.view, animated: true)
}

複製程式碼

效果如下圖

iOS多執行緒詳解:概念篇
  • 6、AirDrop

引自維基百科AirDrop是蘋果公司的MacOS和iOS作業系統中的一個隨建即連網路,自Mac OS X Lion(Mac OS X 10.7)和iOS 7起引入,允許在支援的麥金塔計算機和iOS裝置上傳輸檔案,無需透過郵件或大容量儲存裝置。

在OS X Yosemite(OS X 10.10)之前,OS X 中的隔空投送協議不同於iOS的隔空投送協議,因此不能互相傳輸[2]。但是,OS X Yosemite或更新版本支援iOS的隔空投送協議(使用Wi-Fi和藍芽),這適用於一臺Mac與一臺iOS裝置以及兩臺2012年或更新版本的Mac計算機之間的傳輸。[3][4]使用舊隔空投送協議(只使用Wi-Fi)的舊模式在兩臺2012年或更早的Mac計算機之間傳輸也是可行的。[4]

隔空投送所容納的檔案大小沒有限制。蘋果使用者報告稱隔空投送能傳輸小於10GB的視訊檔案。

iOS並沒有直接提供AirDrop的實現介面,但是使用UIActivityViewController的方法喚起AirDrop,進行資料互動。

  • 7、UIActivityViewController

UIActivityViewController類是一個標準的ViewController,提供了幾項標準的服務,比如複製專案至剪貼簿,把內容分享至社交網站,以及通過Messages傳送資料等等。在iOS 7 SDK中,UIActivityViewController類提供了內建的AirDrop功能。

如果你有一些資料一批物件需要通過AirDrop進行分享,你所需要的是通過物件陣列初始化UIActivityViewController,並展示在螢幕上:

UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil]; 
[self presentViewController:controller animated:YES completion:nil]; 

複製程式碼

效果圖如下

iOS多執行緒詳解:概念篇
  • 8、App Groups

App Group用於同一個開發團隊開發的App之間,包括AppExtension之間共享同一份讀寫空間,進行資料共享。同一個團隊開發的多個應用之間如果能直接資料共享,大大提高使用者體驗。

實現細節參考App之間的資料共享——App Group的配置

執行緒

執行緒是什麼

引自維基百科執行緒(英語:thread)是作業系統能夠進行運算排程的最小單位。它被包含在程式之中,是程式中的實際運作單位。

一條執行緒指的是程式中一個單一順序的控制流,一個程式中可以併發多個執行緒,每條執行緒並行執行不同的任務。在Unix System V及SunOS中也被稱為輕量程式(lightweight processes),但輕量程式更多指核心執行緒(kernel thread),而把使用者執行緒(user thread)稱為執行緒。

講執行緒就不能不提任務,任務是什麼的,通俗的說任務就是就一件事情或一段程式碼,執行緒其實就是去執行這件事情。

執行緒(thread),指的是一個獨立的程式碼執行路徑,也就是說執行緒是程式碼執行路徑的最小分支。在 iOS 中,執行緒的底層實現是基於 POSIX threads API 的,也就是我們常說的 pthreads ;

超執行緒技術

引自維基百科超執行緒(HT, Hyper-Threading)超執行緒技術就是利用特殊的硬體指令,把一個物理核心模擬成兩個邏輯核心,讓單個處理器都能使用執行緒級平行計算,進而相容多執行緒作業系統和軟體,減少了CPU的閒置時間,提高了CPU的執行速度。 採用超執行緒即是可在同一時間裡,應用程式可以使用晶片的不同部分。

引自知乎超執行緒這個東西並不是開了就一定比不開的好。因為每個CPU核心裡ALU,FPU這些運算單元的數量是有限的,而超執行緒的目的之一就是在一個執行緒用運算單元少的情況下,讓另外一個執行緒跑起來,不讓運算單元閒著。但是如果當一個執行緒整數,浮點運算各種多,當前核心運算單元沒多少空閒了,這時候你再塞進了一個執行緒,這下子資源就緊張了。兩執行緒就會互相搶資源,拖慢對方速度。至於,超執行緒可以解決一個執行緒cache miss,另外一個可以頂上,但是如果兩個執行緒都miss了,那就只有都在等了。這個還是沒有GPU裡一個SM裡很多warp,超多執行緒同時跑來得有效果。所以,如果你的程式是單執行緒,關了超執行緒,免得別人搶你資源,如果是多執行緒,每個執行緒運算不大,超執行緒比較有用。

執行緒間通訊

執行緒間通訊的表現為:一個執行緒傳遞資料給另一個執行緒;在一個執行緒中執行完特定任務後,轉到另一個執行緒繼續執行任務。

下面主要是介紹__其他執行緒執行耗時任務,在主執行緒進行UI的重新整理__,也是業務中比較常用的一種。

  • 1、NSThread執行緒間通訊

NSThread是用Swift語言裡的Thread

NSThread這套方案是經過蘋果封裝後,並且完全物件導向的。所以你可以直接操控執行緒物件,非常直觀和方便。不過它的生命週期還是需要我們手動管理,所以實際上使用也比較少,使用頻率較多的是GCD以及NSOperation

當然,NSThread還可以用來做執行緒間通訊,比如下載圖片並展示為例,將下載耗時操作放在子執行緒,下載完成後再切換回主執行緒在UI介面對圖片進行展示

func onThread() {
    let urlStr = "http://tupian.aladd.net/2015/7/2941.jpg"
    self.performSelector(inBackground: #selector(downloadImg(_:)), with: urlStr)
}

@objc func downloadImg(_ urlStr: String) {
    //列印當前執行緒
    print("下載圖片執行緒", Thread.current)
    
    //獲取圖片連結
    guard let url = URL.init(string: urlStr) else {return}
    //下載圖片二進位制資料
    guard let data = try? Data.init(contentsOf: url) else {return}
    //設定圖片
    guard let img = UIImage.init(data: data) else {return}
    
    //回到主執行緒重新整理UI
    self.performSelector(onMainThread: #selector(downloadFinished(_:)), with: img, waitUntilDone: false)
}

@objc func downloadFinished(_ img: UIImage) {
    //列印當前執行緒
    print("重新整理UI執行緒", Thread.current)
}
複製程式碼

下載圖片執行緒 <NSThread: 0x1c4464a00>{number = 5, name = (null)}
重新整理UI執行緒 <NSThread: 0x1c007a400>{number = 1, name = main}

有的小夥伴應該會有疑問,為什麼執行的NSObject的方法實現的執行緒,怎麼變成了NSThread的執行緒呢。
其實這個方法是NSObjectNSThread的封裝,方便快速實現執行緒的方法,下個斷點驗證下看看。

iOS多執行緒詳解:概念篇
  • 2、GCD執行緒間通訊

GCD(Grand Central Dispatch)偉大的中央排程系統,是蘋果為多核並行運算提出的C語言併發技術框架。
GCD會自動利用更多的CPU核心。
會自動管理執行緒的生命週期(建立執行緒,排程任務,銷燬執行緒等)。
程式設計師只需要告訴GCD想要如何執行什麼任務,不需要編寫任何執行緒管理程式碼。

func onThread() {
    let urlStr = "http://tupian.aladd.net/2015/7/2941.jpg"

    let dsp = DispatchQueue.init(label: "com.jk.thread")
    dsp.async {
        self.downloadImg(urlStr)
    }
}

@objc func downloadImg(_ urlStr: String) {
    //列印當前執行緒
    print("下載圖片執行緒", Thread.current)
    
    //獲取圖片連結
    guard let url = URL.init(string: urlStr) else {return}
    //下載圖片二進位制資料
    guard let data = try? Data.init(contentsOf: url) else {return}
    //設定圖片
    guard let img = UIImage.init(data: data) else {return}
    
    //回到主執行緒重新整理UI
    DispatchQueue.main.async {
        self.downloadFinished(img)
    }
}

@objc func downloadFinished(_ img: UIImage) {
    //列印當前執行緒
    print("重新整理UI執行緒", Thread.current)
}

複製程式碼

下載圖片執行緒 <NSThread: 0x1c426b9c0>{number = 4, name = (null)}
重新整理UI執行緒 <NSThread: 0x1c0263140>{number = 1, name = main}

  • 3、NSOperation執行緒間通訊

NSOperation是用Swift語言裡的Operation

NSOperation是蘋果推薦使用的併發技術,它提供了一些用GCD不是很好實現的功能。相比GCDNSOperation的使用更加簡單。NSOperation是一個抽象類,也就是說它並不能直接使用,而是應該使用它的子類。Swift裡面可以使用BlockOperation和自定義繼承自Operation的子類。

NSOperation的使用常常是配合NSOperationQueue來進行的。只要是使用NSOperation的子類建立的例項就能新增到NSOperationQueue操作佇列之中,一旦新增到佇列,操作就會自動非同步執行(注意是非同步)。如果沒有新增到佇列,而是使用start方法,則會在當前執行緒執行。

func onThread() {
    let urlStr = "http://tupian.aladd.net/2015/7/2941.jpg"

    let que = OperationQueue.init()
    que.addOperation {
        self.downloadImg(urlStr)
    }
}

@objc func downloadImg(_ urlStr: String) {
    //列印當前執行緒
    print("下載圖片執行緒", Thread.current)
    
    //獲取圖片連結
    guard let url = URL.init(string: urlStr) else {return}
    //下載圖片二進位制資料
    guard let data = try? Data.init(contentsOf: url) else {return}
    //設定圖片
    guard let img = UIImage.init(data: data) else {return}
    
    //回到主執行緒重新整理UI
    OperationQueue.main.addOperation {
        self.downloadFinished(img)
    }
}

@objc func downloadFinished(_ img: UIImage) {
    //列印當前執行緒
    print("重新整理UI執行緒", Thread.current)
}
複製程式碼

下載圖片執行緒 <NSThread: 0x1c4271d80>{number = 3, name = (null)}
重新整理UI執行緒 <NSThread: 0x1c006cac0>{number = 1, name = main}

OperationQueue的物件執行addOperation的方法,其實是生成了一個BlockOperation物件,非同步執行當前任務。
下個斷點,可以看到BlockOperation的執行過程。

iOS多執行緒詳解:概念篇

執行緒池

執行緒池(英語:thread pool):一種執行緒使用模式。 執行緒過多會帶來排程開銷,進而影響快取區域性性和整體效能。 而執行緒池維護著多個執行緒,等待著監督管理者分配可併發執行的任務。 這避免了在處理短時間任務時建立與銷燬執行緒的代價。

執行緒池的執行流程如:
首先,啟動若干數量的執行緒,並讓這些執行緒處於睡眠狀態
其次,當客戶端有新的請求時,執行緒池會喚醒某一個睡眠執行緒,讓它來處理客戶端的請求
最後,當請求處理完畢,執行緒又處於睡眠狀態

所以在併發的時候,同時能有多少執行緒在跑是有執行緒池的執行緒快取數量決定的。

  • 1、GCD

GCD有一個底層執行緒池,這個池中存放的是一個個的執行緒。之所以稱為“池”,很容易理解出這個“池”中的執行緒是可以重用的,當一段時間後這個執行緒沒有被呼叫的話,這個執行緒就會被銷燬。池是系統自動來維護,不需要我們手動來維護。

GCD底層執行緒池的快取數到底有多少個的,寫段程式碼跑一下看看。

@IBAction func onThread() {
    let dsp = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    for i in 0..<10000 {
        dsp.async {
            //列印當前執行緒
            print("(i)當前執行緒", Thread.current)
            //耗時任務
            Thread.sleep(forTimeInterval: 5)
        }
    }
}

複製程式碼
iOS多執行緒詳解:概念篇

這段程式碼是生成了一個併發佇列,for迴圈10000次,執行非同步任務,相當於會生成10000條執行緒,由於非同步執行任務,會立即執行而且不會等待任務的結束,所以在生成執行緒的時候,執行緒列印就立即執行了。從列印的結果來看,一次列印總共有64行,從而可以得出GCD的執行緒池快取數量是64
當然如果想實現更多執行緒的併發執行的話,可以使用開源的YYDispatchQueuePool

  • 2、NSOperation

NSOperationQueue提供了一套類似於執行緒池的機制,通過它可以更加方便的進行多執行緒的併發操作,構造一個執行緒池並新增任務物件到執行緒池中,執行緒池會分配執行緒,呼叫任務物件的main方法執行任務。

下面寫段程式碼,分配maxConcurrentOperationCount3個,看看效果

@IBAction func onThread() {
    let opq = OperationQueue.init()
    opq.maxConcurrentOperationCount = 3
    for i in 0..<10 {
        opq.addOperation({
            //列印當前執行緒
            print("(i)當前執行緒", Thread.current)
            //耗時任務
            Thread.sleep(forTimeInterval: 5)
        })
    }
}
複製程式碼

1當前執行緒 <NSThread: 0x1c0274a80>{number = 4, name = (null)}
0當前執行緒 <NSThread: 0x1c4673900>{number = 6, name = (null)}
2當前執行緒 <NSThread: 0x1c4674040>{number = 7, name = (null)}

可以看到,規定執行緒池快取為3個,一次就列印3個執行緒,當這3個執行緒回收到執行緒池裡,又會再列印3個,當然__如果其中一個執行緒先執行完,他就會先被回收__。

NSOperationQueue一次能夠併發執行多少執行緒呢,跑一下下面程式碼

@IBAction func onThread() {
    let opq = OperationQueue.init()
    opq.maxConcurrentOperationCount = 300
    for i in 0..<100 {
        opq.addOperation({
            //列印當前執行緒
            print("(i)當前執行緒", Thread.current)
            //耗時任務
            Thread.sleep(forTimeInterval: 5)
        })
    }
}

複製程式碼
iOS多執行緒詳解:概念篇

可以看到也是64個,也就是__NSOperationQueue多了可以操作執行緒數量的介面,但是最大的執行緒併發數量還是64個__。

多執行緒同步

1、多執行緒同步是什麼

引自百度百科同步就是協同步調,按預定的先後次序進行執行。可理解為執行緒A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B執行;B依言執行,再將結果給A;A再繼續操作。

執行緒同步其實是對於併發佇列說的,序列佇列的任務是依次執行的,本身就是同步的。

iOS多執行緒詳解:概念篇
2、多執行緒同步用途

結果傳遞:A執行到一定程度時要依靠B的某個結果,於是停下來,示意B執行;B依言執行,再將結果給A;A再繼續操作。例子,小明、小李有三個西瓜,這個三個西瓜可以同時切開(併發),全部切完之後放到冰箱裡冰好(同步),小明、小李吃冰西瓜(併發)。

資源競爭:是指多個執行緒同時訪問一個資源時可能存在競爭問題提供的解決方案,使多個執行緒可以對同一個資源進行操作,比如執行緒A為陣列M新增了一個資料,執行緒B可以接收到新增資料後的陣列M。執行緒同步就是執行緒之間相互的通訊。例子,購買火車票,多個視窗賣票(併發),票賣出去之後要把庫存減掉(同步),多個視窗出票成功(併發)。

3、多執行緒同步實現

多執行緒同步實現的方式有很多訊號量(DispatchSemaphore)、鎖(NSLock)、@synchronizeddispatch_barrier_asyncaddDependencypthread_mutex_t,下面用這個方式實現火車票的去庫存的情況。

  • DispatchSemaphore

GCD中訊號量,也可以解決資源搶佔問題,支援訊號通知和訊號等待。每當傳送一個訊號通知,則訊號量+1;每當傳送一個等待訊號時訊號量-1;如果訊號量為0則訊號會處於等待狀態,直到訊號量大於0開始執行。

簡單地說就是洗手間只有一個坑位,外面進來一個人把門關上,其他人排隊,這個人把門開啟出去之後,可以再進來一個人。程式碼例子如下。

var tickets: [Int] = [Int]()

@IBAction func onThread() {
    let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    //北京賣票視窗
    que.async {
        self.saleTicket()
    }
    
    //上海賣票視窗
    que.async {
        self.saleTicket()
    }
}

//存在一個坑位
let semp = DispatchSemaphore.init(value: 1)

func saleTicket() {
    while true {
        //佔坑,坑位減一
        semp.wait()
        
        if tickets.count > 0 {
            print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else {
            print("票已經賣完了")
            
            //釋放佔坑,坑位加一
            semp.signal()
            
            break
        }
        
        //釋放佔坑,坑位加一
        semp.signal()
    }
}

複製程式碼

列印結果:
剩餘票數 100 賣票視窗 <NSThread: 0x1c0472e40>{number = 6, name = (null)}
剩餘票數 99 賣票視窗 <NSThread: 0x1c027b540>{number = 4, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c0472e40>{number = 6, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c027b540>{number = 4, name = (null)}
剩餘票數 96 賣票視窗 <NSThread: 0x1c0472e40>{number = 6, name = (null)}
剩餘票數 95 賣票視窗 <NSThread: 0x1c027b540>{number = 4, name = (null)}
……………..
剩餘票數 4 賣票視窗 <NSThread: 0x1c0472e40>{number = 6, name = (null)}
剩餘票數 3 賣票視窗 <NSThread: 0x1c027b540>{number = 4, name = (null)}
剩餘票數 2 賣票視窗 <NSThread: 0x1c0472e40>{number = 6, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c027b540>{number = 4, name = (null)}
票已經賣完了
票已經賣完了

在不使用訊號量的情況下,執行一段時間就會崩潰,這是多執行緒同事操作tickets票池的removeLast去庫存的方法引起的,這樣顯然不符合我們的需求,所以我們需要考慮執行緒安全問題。

  • NSLock

鎖的概念,鎖是最常用的同步工具。一段程式碼段在同一個時間只能允許被一個執行緒訪問,比如一個執行緒A進入加鎖程式碼之後由於已經加鎖,另一個執行緒B就無法訪問,只有等待前一個執行緒A執行完加鎖程式碼後解鎖,B執行緒才能訪問加鎖程式碼。
不要將過多的其他操作程式碼放到裡面,否則一個執行緒執行的時候另一個執行緒就一直在等待,就無法發揮多執行緒的作用了。

Cocoa程式中NSLock中實現了一個簡單的互斥鎖,實現了NSLocking Protocol。實現程式碼如下:

var tickets: [Int] = [Int]()

@IBAction func onThread() {
    let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    //北京賣票視窗
    que.async {
        self.saleTicket()
    }
    
    //上海賣票視窗
    que.async {
        self.saleTicket()
    }
}

//生成一個鎖
let lock = NSLock.init()

func saleTicket() {
    while true {
        //關門,執行任務
        lock.lock()
        
        if tickets.count > 0 {
            print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else {
            print("票已經賣完了")
            
            //開門,讓其他任務可以執行
            lock.unlock()
            
            break
        }
        
        //開門,讓其他任務可以執行
        lock.unlock()
    }
}

複製程式碼

列印結果:
剩餘票數 100 賣票視窗 <NSThread: 0x1c467d300>{number = 6, name = (null)}
剩餘票數 99 賣票視窗 <NSThread: 0x1c4862380>{number = 7, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c467d300>{number = 6, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c4862380>{number = 7, name = (null)}
剩餘票數 96 賣票視窗 <NSThread: 0x1c467d300>{number = 6, name = (null)}
剩餘票數 95 賣票視窗 <NSThread: 0x1c4862380>{number = 7, name = (null)}
……………..
剩餘票數 4 賣票視窗 <NSThread: 0x1c467d300>{number = 6, name = (null)}
剩餘票數 3 賣票視窗 <NSThread: 0x1c4862380>{number = 7, name = (null)}
剩餘票數 2 賣票視窗 <NSThread: 0x1c467d300>{number = 6, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c4862380>{number = 7, name = (null)}
票已經賣完了
票已經賣完了

  • @synchronized

Objective-C中,我們可以用@synchronized關鍵字來修飾一個物件,併為其自動加上和解除互斥鎖。
但是在Swift中,沒有與之對應的方法,即@synchronizedSwift中已經(或者是暫時)不存在了。其實@synchronized在幕後做的事情是呼叫了objc_sync中的objc_sync_enterobjc_sync_exit方法,我可以直接呼叫這兩個方法去實現。

var tickets: [Int] = [Int]()
    
@IBAction func onThread() {
    let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    //北京賣票視窗
    que.async {
        self.saleTicket()
    }
    
    //上海賣票視窗
    que.async {
        self.saleTicket()
    }
}

func saleTicket() {
    while true {
        //加鎖,關門,執行任務
        objc_sync_enter(self)
        
        if tickets.count > 0 {
            print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else {
            print("票已經賣完了")
            
            //開鎖,開門,讓其他任務可以執行
            objc_sync_exit(self)
            
            break
        }
        
        //開鎖,開門,讓其他任務可以執行
        objc_sync_exit(self)
    }
}
複製程式碼

列印結果:
剩餘票數 100 賣票視窗 <NSThread: 0x1c04697c0>{number = 6, name = (null)}
剩餘票數 99 賣票視窗 <NSThread: 0x1c44706c0>{number = 4, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c04697c0>{number = 6, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c44706c0>{number = 4, name = (null)}
剩餘票數 96 賣票視窗 <NSThread: 0x1c04697c0>{number = 6, name = (null)}
……………..
剩餘票數 3 賣票視窗 <NSThread: 0x1c44706c0>{number = 4, name = (null)}
剩餘票數 2 賣票視窗 <NSThread: 0x1c04697c0>{number = 6, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c44706c0>{number = 4, name = (null)}
票已經賣完了
票已經賣完了

  • GCD 柵欄方法:dispatch_barrier_async

我們有時需要非同步執行兩組操作,而且第一組操作執行完之後,才能開始執行第二組操作。這樣我們就需要一個相當於 柵欄 一樣的一個方法將兩組非同步執行的操作組給分割起來,當然這裡的操作組裡可以包含一個或多個任務。這就需要用到dispatch_barrier_async方法在兩個操作組間形成柵欄。
dispatch_barrier_async函式會等待前邊追加到併發佇列中的任務全部執行完畢之後,再將指定的任務追加到該非同步佇列中。然後在dispatch_barrier_async函式追加的任務執行完畢之後,非同步佇列才恢復為一般動作,接著追加任務到該非同步佇列並開始執行。

var tickets: [Int] = [Int]()

@IBAction func onThread() {
    let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    for _ in 0..<51 {
        //北京賣票視窗
        que.async {
            self.saleTicket()
        }
        
        //GCD 柵欄方法,同步去庫存
        que.async(flags: .barrier) {
            if self.tickets.count > 0 {
                self.tickets.removeLast()
            }
        }
        
        //上海賣票視窗
        que.async {
            self.saleTicket()
        }
        
        //GCD 柵欄方法,同步去庫存
        que.async(flags: .barrier) {
            if self.tickets.count > 0 {
                self.tickets.removeLast()
            }
        }
    }
}

func saleTicket() {
    if tickets.count > 0 {
        print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
        Thread.sleep(forTimeInterval: 0.2)
    }
    else {
        print("票已經賣完了")
    }
}

複製程式碼

列印結果:
剩餘票數 100 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
剩餘票數 99 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
……………..
剩餘票數 59 賣票視窗 <NSThread: 0x1c0670100>{number = 6, name = (null)}
剩餘票數 58 賣票視窗 <NSThread: 0x1c0670100>{number = 6, name = (null)}
剩餘票數 57 賣票視窗 <NSThread: 0x1c0670100>{number = 6, name = (null)}
剩餘票數 56 賣票視窗 <NSThread: 0x1c0670100>{number = 6, name = (null)}
……………..
剩餘票數 3 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
剩餘票數 2 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c0463c40>{number = 3, name = (null)}
票已經賣完了
票已經賣完了

  • addDependency(操作依賴)

NSOperation、NSOperationQueue最吸引人的地方是它能新增操作之間的依賴關係。通過操作依賴,我們可以很方便的控制操作之間的執行先後順序。下面使用操作依賴實現多執行緒同步,程式碼如下。

var tickets: [Int] = [Int]()

@IBAction func onThread() {
    let que = OperationQueue.init()//併發佇列
    que.maxConcurrentOperationCount = 1
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    for _ in 0..<51 {
        //addDependency方法,同步去庫存
        let sync1 = BlockOperation.init(block: {
            if self.tickets.count > 0 {
                self.tickets.removeLast()
            }
        })
        
        //北京賣票視窗
        let bj = BlockOperation.init(block: {
            self.saleTicket()
        })
        bj.addDependency(sync1)//等待去庫存
        
        //addDependency方法,同步去庫存
        let sync2 = BlockOperation.init(block: {
            if self.tickets.count > 0 {
                self.tickets.removeLast()
            }
        })
        
        //上海賣票視窗
        let sh = BlockOperation.init(block: {
            self.saleTicket()
        })
        sh.addDependency(sync2)//等待去庫存
        
        que.addOperation(sync1)
        que.addOperation(bj)
        que.addOperation(sync2)
        que.addOperation(sh)
    }
}

func saleTicket() {
    if tickets.count > 0 {
        print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
        Thread.sleep(forTimeInterval: 0.2)
    }
    else {
        print("票已經賣完了")
    }
}

複製程式碼

列印結果:
剩餘票數 99 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c06731c0>{number = 5, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c06731c0>{number = 5, name = (null)}
剩餘票數 96 賣票視窗 <NSThread: 0x1c06731c0>{number = 5, name = (null)
……………..
剩餘票數 54 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
剩餘票數 53 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
剩餘票數 52 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
……………..
剩餘票數 2 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c42672c0>{number = 4, name = (null)}
票已經賣完了
票已經賣完了
票已經賣完了

  • 使用POSIX互斥鎖

POSIX互斥鎖在很多程式裡面很容易使用。為了新建一個互斥鎖,你宣告並初始化一個pthread_mutex_t的結構。為了鎖住和解鎖一個互斥鎖,你可以使用pthread_mutex_lock和pthread_mutex_unlock函式。列表4-2顯式了要初始化並使用一個POSIX執行緒的互斥鎖的基礎程式碼。當你用完一個鎖之後,只要簡單的呼叫pthread_mutex_destroy來釋放該鎖的資料結構。

var tickets: [Int] = [Int]()

@IBAction func onThread() {
    let que = DispatchQueue.init(label: "com.jk.thread", attributes: .concurrent)
    
    mutex()
    
    //生成100張票
    for i in 0..<100 {
        tickets.append(i)
    }
    
    //北京賣票視窗
    que.async {
        self.saleTicket()
    }
    
    //上海賣票視窗
    que.async {
        self.saleTicket()
    }
}

//生成一個鎖
var lock = pthread_mutex_t.init()

func mutex() {
    //設定屬性
    var attr: pthread_mutexattr_t = pthread_mutexattr_t()
    pthread_mutexattr_init(&attr)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)
    let err = pthread_mutex_init(&self.lock, &attr)
    pthread_mutexattr_destroy(&attr)
    
    switch err {
    case 0:
        // Success
        break
        
    case EAGAIN:
        fatalError("Could not create mutex: EAGAIN (The system temporarily lacks the resources to create another mutex.)")
        
    case EINVAL:
        fatalError("Could not create mutex: invalid attributes")
        
    case ENOMEM:
        fatalError("Could not create mutex: no memory")
        
    default:
        fatalError("Could not create mutex, unspecified error (err)")
    }
}


func saleTicket() {
    while true {
        //關門,執行任務
        pthread_mutex_lock(&lock)
        
        if tickets.count > 0 {
            print("剩餘票數", tickets.count, "賣票視窗", Thread.current)
            tickets.removeLast()
            Thread.sleep(forTimeInterval: 0.2)
        }
        else {
            print("票已經賣完了")
            
            //開門,讓其他任務可以執行
            pthread_mutex_unlock(&lock)
            
            break
        }
        
        //開門,讓其他任務可以執行
        pthread_mutex_unlock(&lock)
    }
}

deinit {
    pthread_mutex_destroy(&lock)
}
複製程式碼

列印結果:
剩餘票數 100 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
剩餘票數 99 賣票視窗 <NSThread: 0x1c0472900>{number = 6, name = (null)}
剩餘票數 98 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
剩餘票數 97 賣票視窗 <NSThread: 0x1c0472900>{number = 6, name = (null)}
剩餘票數 96 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
……………..
剩餘票數 35 賣票視窗 <NSThread: 0x1c0472900>{number = 6, name = (null)}
剩餘票數 34 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
剩餘票數 33 賣票視窗 <NSThread: 0x1c0472900>{number = 6, name = (null)}
剩餘票數 32 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
……………..
剩餘票數 2 賣票視窗 <NSThread: 0x1c446f7c0>{number = 5, name = (null)}
剩餘票數 1 賣票視窗 <NSThread: 0x1c0472900>{number = 6, name = (null)}
票已經賣完了
票已經賣完了

可以看到使用pthread_mutex_t完成了多執行緒同步,當然實現鎖的型別有很多,比如還有NSRecursiveLock(遞迴鎖)、NSConditionLock(條件鎖)、NSDistributedLock(分散式鎖)、OSSpinLock(自旋鎖)等方式,就不用程式碼一一實現了。

同非同步和佇列

同非同步是什麼

步和非同步操作的主要區別在於是否等待操作執行完成,亦即是否阻塞當前執行緒。同步操作會等待操作執行完成後再繼續執行接下來的程式碼,而非同步操作則恰好相反,它會在呼叫後立即返回,不會等待操作的執行結果。

同步 非同步
是否阻塞當前執行緒
是否等待任務執行完成

佇列是什麼

  • 佇列含義

引自維基百科佇列,又稱為佇列(queue),是先進先出(FIFO, First-In-First-Out)的線性表。在具體應用中通常用連結串列或者陣列來實現。佇列只允許在後端(稱為rear)進行插入操作,在前端(稱為front)進行刪除操作。
佇列的操作方式和堆疊類似,唯一的區別在於佇列只允許新資料在後端進行新增。

iOS多執行緒詳解:概念篇
  • 序列佇列和併發佇列

序列佇列一次只能執行一個任務,而併發佇列則可以允許多個任務同時執行。iOS系統就是使用這些佇列來進行任務排程的,它會根據排程任務的需要和系統當前的負載情況動態地建立和銷燬執行緒,而不需要我們手動地管理。

注意這個併發多個任務同時執行的同時在二字指的是同一時間內(由於CPU執行很快,感覺幾乎同時)

這裡會有一個經常性的疑問,序列佇列一次執行一個任務,任務按順序執行,先進先出,這個好理解。那併發幾個任務同時執行也是先進先出,這個怎麼理解呢。因為併發執行任務,先進去的任務並不一定先執行完,但是即使後面的任務先執行完,也是要等前面的任務退出。這是由佇列的性質決定的。

序列佇列 併發佇列
同步執行 當前執行緒,一個接著一個地執行,順序執行,一個任務執行完畢後,再執行下一個任務 當前執行緒,一個接著一個地執行,順序執行,一個任務執行完畢後,再執行下一個任務
非同步執行 其他執行緒,一個接著一個地執行 多個任務,多個執行緒,多個任務併發執行
iOS多執行緒詳解:概念篇
iOS多執行緒詳解:概念篇

注意這裡說的是佇列執行時間,並不是先進先出的示例。

  • 併發佇列和並行佇列

併發的來歷
在過去單CPU時代,單任務在一個時間點只能執行單一程式。之後發展到多工階段,計算機能在同一時間點並行執行多工或多程式。雖然並不是真正意義上的“同一時間點”,而是多個任務或程式共享一個CPU,並交由作業系統來完成多工間對CPU的執行切換,以使得每個任務都有機會獲得一定的時間片執行。
並行的來歷
多執行緒比多工更加有挑戰。多執行緒是在同一個程式內部並行執行,因此會對相同的記憶體空間進行併發讀寫操作。這可能是在單執行緒程式中從來不會遇到的問題。其中的一些錯誤也未必會在單CPU機器上出現,因為兩個執行緒從來不會得到真正的並行執行。然而,更現代的計算機伴隨著多核CPU的出現,也就意味著__不同的執行緒能被不同的CPU核得到真正意義的並行執行__。

併發的關鍵是你有處理多個任務的能力,不一定要同時。
並行的關鍵是你有同時處理多個任務的能力。
併發是一種能力,處理多個任務的能力。並行是狀態,多個任務同時執行的狀態。
可以看到併發和並行並不是同一類概念,所以不具有比較性,併發包含並行,就比如水果是包含西瓜一樣。併發的不一定是並行,並行的一定是併發。

一個CPU的核心同時只能處理一個執行緒。
單核CPU一個執行緒,當前是並行(兩個以上才叫並行,為了理解,暫且這樣叫吧)。單核CPU兩個執行緒,當前是併發。雙核CPU兩個執行緒,當前是並行。雙核CPU四個執行緒,當前是併發。

iOS多執行緒詳解:概念篇
iOS多執行緒詳解:概念篇

關注我

歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。

iOS多執行緒詳解:概念篇

相關文章