概述html
早上起牀,你先打開洗衣機,而後用熱水把泡麪泡上,接着打開電腦開啓一天的碼農生活。其中「洗衣服」、「泡泡麪」和「碼代碼」3個任務(線程)同時進行,這就是多線程。網上有許多關於多線程的經典解釋,此處就再也不菜鳥弄斧了,以避免貽笑大方。當今流行於世的系統基本都會提供多線程這項基本功能,iOS也不例外。其中Swift提供了3種可選方案:NSThread,GCD和NSOperation,接下來咱們將對3種方案進行運用和分析。安全
NSThread多線程
NSThread是Objective-C給Swift留下的衆多遺產之一,而且Swift給其定義了一個簡體名稱Thread。併發
1 class ViewController: UIViewController { 2 3 @IBOutlet weak var testLabel: UILabel! 4 var testThread:Thread? = nil 5 var count = 0 6 7 override func viewDidLoad() { 8 super.viewDidLoad() 9 testLabel.text = "Charpter9" 10 11 testThread = Thread.init(target: self, selector: #selector(threadFunc), object: "菜鳥先飛") 12 testThread!.start() 13 } 14 15 override func didReceiveMemoryWarning() { 16 super.didReceiveMemoryWarning() 17 // Dispose of any resources that can be recreated. 18 } 19 20 21 @objc func threadFunc(p: String) { 22 23 while(true) { 24 sleep(1) 25 count += 1 26 print("\(p): \(count)") 27 } 28 } 29 }
建立線程使用Thread.init(target:Any, selector: Selector, object: Any?)。app
其中target表示selector函數所屬的類,selector表示線程的函數名,object表示函數參數。異步
如上代碼所示,當咱們調用testFunc!.start()時,系統就會建立線程並執行threadFunc函數。async
(注意gif右下角打印變化)ide
提示:針對target參數,網上有個千篇一概的說法是:「selector消息發送的對象」。針對這個解釋,我只能說「很是忠於原文」,基本就是直譯apple的文檔。target其實就是selector函數所在的類實例,爲何要說的這麼複雜呢?這是由於selector自己並不是函數入口地址,而是1個字符串。線程啓動時,selector字符串被交給了target,target調用和selector字符串同名的方法,進而啓動線程,因此纔有以前的那個晦澀的說法。函數
NSThread是1個輕量級線程調用(相對GCD和NSOperation而言),不帶任何「贈品」。若是你要作數據同步,那你得本身加同步鎖或信號量(NSLock和NSCondition);線程不用了要記得cancel掉,否則會內存泄漏。總之你得留個心眼兒好好管着NSThread這個熊孩子。ui
「什麼?!內存泄漏?!NSThread太可怕了,又很差管,有沒有安全聽話一點的東東啊?」
GCD(Grand Central Dispatch)
這是iOS開發中出鏡率最高的一種線程機制。以下圖所示,
GCD涉及1個先入先出(FIFO)隊列,任務依次入隊,再依次出隊交給線程執行。整個過程當中,除了建立隊列、新任務和加入任務到隊列的動做外,其餘均由系統本身處理。FIFO隊列和線程的生老病死養老送終都由系統負責解決。真是名副其實的「大中央調度」。接下來咱們看看GCD有哪些特性以及如何使用。
1 import UIKit 2 3 class ViewController: UIViewController { 4 5 var queue: DispatchQueue? 6 7 override func viewDidLoad() { 8 super.viewDidLoad() 9 /* create serial queue */ 10 queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial") 11 12 print("start test sync...") 13 for i in 1...3 { 14 print("sync start \(i)") 15 queue?.sync { 16 sleep(1) 17 let df = DateFormatter() 18 df.dateFormat = "HH:mm:ss" 19 print("sync executing:\(Thread.current),[\(df.string(from: Date()))]") 20 } 21 print("sync end \(i)") 22 } 23 print("start test async...") 24 for i in 1...3 { 25 print("async start \(i)") 26 queue?.async { 27 sleep(1) 28 let df = DateFormatter() 29 df.dateFormat = "HH:mm:ss" 30 print("async executing:\(Thread.current),[\(df.string(from: Date()))]") 31 } 32 print("async end \(i)") 33 } 34 35 } 36 37 override func didReceiveMemoryWarning() { 38 super.didReceiveMemoryWarning() 39 // Dispose of any resources that can be recreated. 40 } 41 42 43 }
首先咱們先建立1個隊列,label爲隊列的惟一標識:
queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial")
DispatchQueue有2個將任務加入隊列的函數:sync(同步)和async(異步)。這二者有什麼區別呢?
在以上代碼中,先是循環入隊3個任務,並同步執行,而後再循環入隊3個任務,並異步執行,每一個任務先sleep一秒鐘,而後打印線程和時間信息。
咱們不妨看看以上代碼的運行結果。
咱們能夠發現「sync」是阻塞的,至關於把任務中的代碼原地執行一邊,和直接調用1個函數沒多大區別。可是「async」就不同了,它將任務入隊就馬上返回,無需等待任務執行完畢。再看看打印信息,咱們能夠發現「sync」執行時,直接使用的是主程序所在的main線程,而「async」則重開了1個新線程。
GCD有2個重要特性,第一個就是「同步」和「異步」。
同步:任務入隊後在當前線程下阻塞執行,不開啓新線程。
異步:任務入隊後不在當前線程下執行,而是開啓新線程,將任務出隊到新線程中執行。
若是咱們再仔細思考思考「async」執行時打印的信息,咱們會發現3個任務是1個接着1個執行的(根據打印時間)。咱們不由會想,系統咋就這麼摳門,異步執行只開了1個線程。假如我如今有急事,想讓它們同時執行要怎麼作呢?
咱們只須要將
queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial") //建立串行隊列
改成
queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.concurrent", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) // 建立併發隊列
就能夠了。
假如我想讓3個任務一塊兒執行要怎麼作呢?
咱們再來看看運行結果
根據打印時間,我能夠發現3個同步執行的任務和以前沒有多大區別,可是3個異步執行的任務是同時執行的,並且系統開了3個線程。
GCD的第二個重要特性是:串行隊列和併發隊列。
串行隊列:只綁定了1個線程,前一個任務執行完畢後,下一個任務才能出隊給綁定線程執行。
併發隊列:根據須要能夠綁定多個線程,無論前一個任務是否執行完畢,只要當前有空閒線程,就將任務出隊給空閒線程執行。
關於DispatchQueue.init(label: "com.ansersion.charpter9.queue.concurrent", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit,target: nil)
這是一個比較新的init接口,日後會繼續傳承仍是遺棄塵封都還無定數,截至博主發文日期,apple官網上關於這個接口的描述仍是個空頁面。可是咱們要記住attributes這個參數「.concurrent」:表示併發隊列。
那麼併發隊列最多能異步併發多少個線程呢?
stackoverflow上有人作了個實驗,發現最多能夠實現66個線程併發,這位同窗真的很6。然而apple並未給出官方說法,66權且做爲1個參考。
如今咱們總結一下:
其實咱們大可沒必要本身建立隊列,系統自己就爲app建立了2個隊列:1個是主線程串行隊列,1個是全局併發隊列。
咱們來說講主線程隊列,app的全部UI更新都是在主線程中進行的。以上咱們的實驗之因此只靠打印來查看效果是由於其餘線程沒法更新UI,不然就崩潰伺候。
若是我想在主線程之外的其餘線程裏更新UI要怎麼辦呢?
這時候就要靠主線程串行隊列了,由於它綁定的是主線程,因此更新UI的任務交給它執行就不會出問題啦。
咱們在以前的代碼中,再後綴如下內容
queue?.async {
sleep(5)
DispatchQueue.main.async {
self.navigationItem.title = "update UI"
}
}
程序啓動5秒鐘以後,你會發現導航欄的標題改變了。
注意:不能在主線程中使用主線程隊列調用sync,不然直接死鎖,由於你已經在執行主線程了,主線程並不空閒,你不能再同步調用它了(不容易理解,需細細體會)。
另外,GCD還能夠延時執行任務,分組執行(挨組執行,每組能夠有多個任務),這些可使用DispatchWorkItem做爲任務單元或者爲async添加參數實現。
此處僅以拋磚引玉,不做細說。
NSOperation
NSOperation自己是一個抽象類,咱們經過使用其現成的子類(BlockOperation)或繼承它自定義子類來實現1個操做(或任務),而後咱們就能夠直接運行這個操做,或者將其放入操做隊列裏執行。
也許你已經發現,其操做和操做隊列的概念和GCD的任務和任務隊列的概念很像。NSOperation官文文檔提到:
An operation queue executes its operations either directly, by running them on secondary threads, or indirectly using the libdispatch library (also known as Grand Central Dispatch)
簡言之,NSOperation是對GCD的封裝。
既然是一脈相承,那麼NSOperation是個長江後浪推前浪的勇進後生呢,仍是說只是個既生瑜何生亮的花瓶?
實現真正的同步併發
如前文所述,在GCD中,不管是串行隊列仍是併發隊列,當咱們調用同步運行(sync)時,都只至關於原地調用線程的代碼。並不存在什麼併發之說。如今咱們來看看NSOperation是怎麼實現同步併發的。
1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 let blockOperation = BlockOperation(); 8 9 for i in 1...3 { 10 print("block \(i) added") 11 blockOperation.addExecutionBlock { 12 sleep(1) 13 let df = DateFormatter() 14 df.dateFormat = "HH:mm:ss" 15 print("BlockOperation addExecutionBlock executing:\(Thread.current),[\(df.string(from: Date()))]") 16 } 17 } 18 19 print("start BlockOperation") 20 blockOperation.start() 21 print("end BlockOperation") 22 } 23 24 override func didReceiveMemoryWarning() { 25 super.didReceiveMemoryWarning() 26 // Dispose of any resources that can be recreated. 27 } 28 }
此處咱們使用NSOperation的子類BlockOperation,經過調用其接口addExecutionBlock添加了3個操做,每一個操做先sleep一秒鐘,而後打印線程和時間信息。經過打印,咱們看到3個任務是使用了3個線程同時執行的,因此是併發的。由於print("end BlockOperation")是在3個線程打印結束後才執行的,因此是同步的。
實現操做依賴
假使如今咱們要開發某款APP,該APP有5個模塊,每一個模塊都有1個線程,如今咱們將各個模塊分配給5個工程師去開發。
正當咱們在爲本身優秀的項目管理和分工能力而沾沾自喜的時候。開發A模塊的工程師告訴你:「我要等到B模塊的運行結果後才能開始啓動,不然#¥**&@%^...」。你感受沒什麼難度,因而爽快的修改了一下主程序再加了一些線程通訊的內容,把A放在B以前運行……這雖然只是你主程序修改的一小步,但倒是你悲劇命運的一大步。時過境遷,滄海桑田,你的APP從5個模塊變成了50個模塊,當這時再有某個工程師找到你,敢問你爽直的豪情是否依舊?
終於有一天,你受不了大喊一聲:「大家能不能本身折騰,別來找我?!」
"操做依賴,你值得擁有」
1 class ViewController: UIViewController { 2 3 override func viewDidLoad() { 4 super.viewDidLoad() 5 let queue = OperationQueue.init() 6 let df = DateFormatter() 7 df.dateFormat = "HH:mm:ss" 8 9 let blockOperationSlow = BlockOperation.init(block: { 10 let df = DateFormatter() 11 df.dateFormat = "HH:mm:ss" 12 print("slow start:\(Thread.current),[\(df.string(from: Date()))]") 13 sleep(3) 14 print("slow end:\(Thread.current),[\(df.string(from: Date()))]") 15 }) 16 let blockOperationQuick = BlockOperation.init(block: { 17 let df = DateFormatter() 18 df.dateFormat = "HH:mm:ss" 19 print("quick start:\(Thread.current),[\(df.string(from: Date()))]") 20 sleep(1) 21 print("quick end:\(Thread.current),[\(df.string(from: Date()))]") 22 }) 23 24 blockOperationQuick.addDependency(blockOperationSlow) 25 26 print("add queue start") 27 queue.addOperation(blockOperationQuick) 28 queue.addOperation(blockOperationSlow) 29 print("add queue end") 30 31 } 32 33 override func didReceiveMemoryWarning() { 34 super.didReceiveMemoryWarning() 35 // Dispose of any resources that can be recreated. 36 } 37 }
首先咱們建立了一個操做隊列(OperationQueue),操做隊列都是併發異步執行的,可是能夠設置最大併發數(maxConcurrentOperationCount),若是設置爲1就等因而串行異步執行了。(注:系統提供了一個默認串行異步的操做隊列:主操做隊列OperationQueue.main,修改其maxConcurrentOperationCount沒有任何做用。因爲它使用的是主線程,也就是說能夠經過它修改UI。)
咱們建立了1個slow操做和1個quick操做,而且讓quick操做依賴slow操做。經過打印咱們發現,2個操做是運行在不一樣的線程中的,即使quick操做只須要執行1秒,而slow操做須要執行3秒,可是quick操做仍是等到slow操做執行完以後才啓動的。
那麼假使兩個操做相互依賴會怎麼樣呢?
回答是:都沒法執行。
唉,碼代碼永遠是走在一條追求完美而不得的不歸路上。
支持繼承
這並不是什麼新功能,但卻能幫助咱們實現良好的代碼結構。閒話很少說,代碼見分明。
1 import UIKit 2 3 class MyOperation: Operation { 4 override func main() { 5 print("do MyOperation") 6 } 7 } 8 9 class ViewController: UIViewController { 10 11 override func viewDidLoad() { 12 super.viewDidLoad() 13 let blockOperation = MyOperation(); 14 blockOperation.start() 15 } 16 17 override func didReceiveMemoryWarning() { 18 super.didReceiveMemoryWarning() 19 // Dispose of any resources that can be recreated. 20 } 21 }
線程安全
線程安全是一個伴隨多線程一輩子的話題。其核心問題就是如何保證對共享資源的串行訪問,多線程的一大利器就是併發,而共享資源倒是排斥併發的。併發利器和共享資源的矛盾遍及中外貫穿古今。慣用的作法就是對共享資源加鎖,有鎖的線程訪問共享資源,其餘線程繼續等待(想象一下排隊蹲茅廁的場景)。
Swift提供了兩種鎖,NSLock和NSRecursiveLock,前者是不可遞歸鎖,鎖了一次後必須解開後才能再次上鎖;後者是可遞歸鎖,比NSLock多了一種功能:在同一個線程中,容許連續n次上鎖(相應的也得有n次解鎖)。
線程安全是個高深而細分的主題,此處點到爲止。
源碼下載(NSThread):https://pan.baidu.com/s/1BfVxj9yzkLw22qIk8IAiBg
源碼下載(GCD):https://pan.baidu.com/s/1oiBENGLszz6lb1dJRVnulQ
源碼下載(NSOperation同步併發):https://pan.baidu.com/s/1TLLtuIP1fjluDwjaIWa8Rg
源碼下載(NSOperation操做依賴):https://pan.baidu.com/s/1yAmh1ZLAb2j4zUL2lsU6-Q