2015/12/09
Day 46
今天學習多線程
多線程的優缺點
- 優點
- 充分發揮多核處理器優勢,將不同線程任務分配給不同的處理器,真正進入“並行運算”狀態
- 將耗時的任務分配到其他線程執行,由主線程負責統一更新界面會使應用程序更加流暢,用戶體驗更好
- 當硬件處理器的數量增加,程序會運行更快,而程序無需做任何調整
- 缺點
新建線程會消耗內存空間和CPU時間,線程太多會降低系統的運行性能
iOS的三種多線程技術
- NSThread
- 使用NSThread對象建立一個線程非常方便
- 但是!要使用NSThread管理多個線程非常困難,不推薦使用
- 技巧!使用[NSThread currentThread]跟蹤任務所在線程,適用於這三種技術
- NSOperation/NSOperationQueue
- 是使用GCD實現的一套Objective-C的API
- 是面向對象的線程技術
- 提供了一些在GCD中不容易實現的特性,如:限制最大並發數量、操作之間的依賴關系
- GCD —— Grand Central Dispatch
- 是基於C語言的底層API
- 用Block定義任務,使用起來非常靈活便捷
- 提供了更多的控制能力以及操作隊列中所不能使用的底層函數
- 提示:iOS的開發者,需要了解三種多線程技術的基本使用,因為在實際開發中會根據實際情況選擇不同的多線程技術
GCD基本思想
- GCD的基本思想是就將操作s放在隊列s中去執行
- 操作使用Blocks定義(Swift里用閉包)
- 隊列負責調度任務執行所在的線程以及具體的執行時間
- 隊列的特點是先進先出(FIFO)的,新添加至對列的操作都會排在隊尾
- 提示
GCD的函數都是以dispatch(分派、調度)開頭的
- 隊列
dispatch_queue_t
- 串行隊列,隊列中的任務只會順序執行
- 並行隊列,隊列中的任務通常會並發執行
- 操作
dispatch_async 異步操作,會並發執行,無法確定任務的執行順序
dispatch_sync 同步操作,會依次順序執行,能夠決定任務的執行順序
下面是演練GCD的OC代碼
串行隊列(一個接一個,排隊跑步,保持隊形)
1 NSLog(@"%@", [NSThread currentThread]); 2 dispatch_queue_t q = dispatch_queue_create(“yu1”, DISPATCH_QUEUE_SERIAL); 3 // 非ARC開發時,千萬別忘記release 4 // dispatch_release(q); 5 6 // 串行行隊列的同步任務,同樣會在主線程上運行 7 // 提示:在開發中極少用 8 for (int i = 0; i < 5; ++i) { 9 // 同步任務順序執行 10 dispatch_sync(q, ^{ 11 NSLog(@"%@ %d", [NSThread currentThread], i); 12 }); 13 } 14 15 for (int i = 0; i < 5; ++i) { 16 // 異步任務,並發執行,但是如果在串行隊列中,仍然會依次順序執行 17 dispatch_async(q, ^{ 18 // [NSThread currentThread] 可以在開發中,跟蹤當前線程 19 // num = 1,表示主線程 20 // num = 2,表示第2個子線程。。。 21 NSLog(@"%@ %d", [NSThread currentThread], i); 22 }); 23 }
打印結果如下

然后是並行隊列(並排跑,類似於賽跑)
1 NSLog(@"%@", [NSThread currentThread]); 2 // 特點:沒有隊形,執行順序程序員不能控制! 3 // 應用場景:並發執行任務,沒有先后順序關系 4 // 並行隊列容易出錯!並行隊列不能控制新建線程的數量! 5 dispatch_queue_t q = dispatch_queue_create("yu2", DISPATCH_QUEUE_CONCURRENT); 6 7 for (int i = 0; i < 10; ++i) { 8 // 異步任務 9 dispatch_async(q, ^{ 10 // [NSThread currentThread] 可以在開發中,跟蹤當前線程 11 // num = 1,表示主線程 12 // num = 2,表示第2個子線程。。。 13 NSLog(@"%@ %d", [NSThread currentThread], i); 14 }); 15 } 16 17 for (int i = 0; i < 10; ++i) { 18 // 同步任務順序執行 19 dispatch_sync(q, ^{ 20 NSLog(@"%@ %d", [NSThread currentThread], i); 21 }); 22 }
控制台打印

注意:關於多線程不要只看一次運行結果,像上面的代碼,多運行幾次結果是不一樣的
全局隊列(蘋果為了方便多線程的設計,提供一個全局隊列,供所有的APP共同使用)
1 // 全局隊列與並行隊列的區別 2 // 1> 不需要創建,直接GET就能用 3 // 2> 兩個隊列的執行效果相同 4 // 3> 全局隊列沒有名稱,調試時,無法確認准確隊列 5 NSLog(@"%@", [NSThread currentThread]); 6 // 優先級在開發中一般用DISPATCH_QUEUE_PRIORITY_DEFAULT 7 dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 8 9 for (int i = 0; i < 5; ++i) { 10 // 同步任務順序執行 11 dispatch_sync(q, ^{ 12 NSLog(@"%@ %d", [NSThread currentThread], i); 13 }); 14 } 15 16 for (int i = 0; i < 5; ++i) { 17 dispatch_async(q, ^{ 18 // [NSThread currentThread] 可以在開發中,跟蹤當前線程 19 // num = 1,表示主線程 20 // num = 2,表示第2個子線程。。。 21 NSLog(@"%@ %d", [NSThread currentThread], i); 22 }); 23 }
打印如下

主(線程)隊列,保證操作在主線程上執行
1 // 每一個應用程序都只有一個主線程 2 // 為什么需要在主線程上工作呢? 3 // 在iOS開發中,所有UI的更新工作,都必須在主線程上執行! 4 dispatch_queue_t q = dispatch_get_main_queue(); 5 6 // 主線程是有工作的,而且除非將程序殺掉,否則主線程的工作永遠不會結束! 7 // 同步任務,阻塞了!!! 8 // dispatch_sync(q, ^{ 9 // NSLog(@"come here baby!"); 10 //}); 11 12 // 異步任務,在主線程上運行,同時是保持隊形的 13 for (int i = 0; i < 5; ++i) { 14 dispatch_async(q, ^{ 15 NSLog(@"%@ - %d", [NSThread currentThread], i); 16 }); 17 }
注意阻塞情況,打印如下

接下來用swift將上面4個demo演練了一遍
串行隊列(一個接一個,排隊跑步,保持隊形)
let q = dispatch_queue_create("串行隊列1", DISPATCH_QUEUE_SERIAL)
print("串行隊列,同步任務")
for i in 0 ..< 5 {
//同步任務,順序執行,同一線程上執行(還是在主線程)
dispatch_sync(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
print("串行隊列,異步任務")
for i in 0 ..< 5 {
//異步任務,開一個子線程並發執行,但是如果在串行隊列中,仍然會依次順序執行
// num = 1,表示主線程
// num = 2,表示第2個子線程。。。
dispatch_async(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
控制台打印
並行隊列(並排跑,類似於賽跑)
// 特點:沒有隊形,執行順序程序員不能控制!
// 應用場景:並發執行任務,沒有先后順序關系
// 並行隊列容易出錯!並行隊列不能控制新建線程的數量!
let q = dispatch_queue_create("並行隊列1", DISPATCH_QUEUE_CONCURRENT)
print("並行隊列,異步任務")
for i in 0 ..< 5 {
//異步任務,隨機亂序,並且有N個子線程
dispatch_async(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
print("並行隊列,同步任務")
for i in 0 ..< 5 {
//同步任務,順序執行,同一線程上執行(還是在主線程)
dispatch_sync(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
控制台打印
全局隊列(蘋果為了方便多線程的設計,提供一個全局隊列,供所有的APP共同使用)
// 全局隊列與並行隊列的區別
// 1> 不需要創建,直接GET就能用
// 2> 兩個隊列的執行效果相同
// 3> 全局隊列沒有名稱,調試時,無法確認准確隊列
// 優先級在開發中一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
let q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
print("全局隊列,異步任務")
for i in 0 ..< 5 {
//異步任務,隨機亂序,並且有N個子線程
dispatch_async(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
print("全局隊列,同步任務")
for i in 0 ..< 5 {
//同步任務,順序執行,同一線程上執行(還是在主線程)
dispatch_sync(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
控制台打印

主(線程)隊列,保證操作在主線程上執行
// 每一個應用程序都只有一個主線程
// 為什么需要在主線程上工作呢?
// 在iOS開發中,所有UI的更新工作,都必須在主線程上執行!
let q = dispatch_get_main_queue()
print("主隊列,異步任務")
for i in 0 ..< 5 {
// 異步任務,在主線程上運行,同時是保持隊形的
dispatch_async(q, { () -> Void in
print("\(NSThread.currentThread()) - \(i)")
})
}
// print("主隊列,同步任務,阻塞")
// // 主線程是由工作的,而且除非將程序殺掉,否則主線程的工作永遠不會結束!
// // 阻塞了!!!
// dispatch_sync(q, { () -> Void in
// print("come on baby")
// })
控制台打印

dispatch_sync的應用場景
- 阻塞並行隊列的執行,要求某一操作執行后再進行后續操作,如用戶登錄
- 確保塊代碼之外的局部變量確實被修改
1 dispatch_queue_t q = dispatch_queue_create(“yu3”, DISPATCH_QUEUE_CONCURRENT); 2 __block BOOL logon = NO; 3 dispatch_sync(q, ^{ 4 NSLog(@"模擬耗時操作 %@", [NSThread currentThread]); 5 [NSThread sleepForTimeInterval:2.0f];//通常在多線程調試中用於模擬耗時操作, 在發布的應用程序中,不要使用此方法! 6 NSLog(@"模擬耗時完成 %@", [NSThread currentThread]); 7 logon = YES; 8 }); 9 10 dispatch_async(q, ^{ 11 NSLog(@"登錄完成的處理 %@", [NSThread currentThread]); 12 });
GCD小結
串行隊列,同步任務,不需要新建線程
串行隊列,異步任務,需要一個子線程,線程的創建和回收不需要程序員參與!
“是最安全的一個選擇”串行隊列只能創建一個子線程
並行隊列,同步任務,不需要創建線程
並行隊列,異步任務,有多少個任務,就開N個線程執行,
無論什么隊列和什么任務,線程的創建和回收不需要程序員參與。
線程的創建回收工作是由隊列負責的
“並發”編程,為了讓程序員從負責的線程控制中解脫出來!只需要面對隊列和任務!
- GCD
- 通過GCD,開發者不用再直接跟線程打交道,只需要向隊列中添加代碼塊即可
- GCD在后端管理着一個線程池,GCD不僅決定着代碼塊將在哪個線程被執行,它還根據可用的系統資源對這些線程進行管理。從而讓開發者從線程管理的工作中解放出來,通過集中的管理線程,緩解大量線程被創建的問題
- 使用GCD,開發者可以將工作考慮為一個隊列,而不是一堆線程,這種並行的抽象模型更容易掌握和使用
- GCD的隊列
- GCD公開有5個不同的隊列:運行在主線程中的主隊列,3 個不同優先級的后台隊列,以及一個優先級更低的后台隊列(用於 I/O)
- 自定義隊列:串行和並行隊列。自定義隊列非常強大,建議在開發中使用。在自定義隊列中被調度的所有Block最終都將被放入到系統的全局隊列中和線程池中
- 提示:不建議使用不同優先級的隊列,因為如果設計不當,可能會出現優先級反轉,即低優先級的操作阻塞高優先級的操作
GCD隊列示意圖

NSOperation & NSOperationQueue
- 簡介
- NSOperationQueue(操作隊列)是由GCD提供的隊列模型的Cocoa抽象,是一套Objective-C的API
- GCD提供了更加底層的控制,而操作隊列則在GCD之上實現了一些方便的功能,這些功能對於開發者而言通常是最好最安全的選擇
- 隊列及操作
- NSOperationQueue有兩種不同類型的隊列:主隊列和自定義隊列
- 主隊列運行在主線程上
- 自定義隊列在后台執行
- 隊列處理的任務是NSOperation的子類
- NSInvocationOperation
- NSBlockOperation
NSOperation的基本使用步驟
- 定義操作隊列
- 定義操作
- 將操作添加到隊列
提示:一旦將操作添加到隊列,操作就會立即被調度執行
NSInvocationOperation(調度操作)
-
1 //定義隊列 2 self.myQueue = [[NSOperationQueue alloc] init]; 3 //操作調用的方法 4 - (void)operationAction:(id)obj 5 { 6 NSLog(@"%@ - obj : %@", [NSThread currentThread], obj); 7 } 8 //定義操作並添加到隊列 9 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@(i)]; 10 [self.myQueue addOperation:op]; 11 //小結:需要准備一個被調度的方法,並且能夠接收一個參數,使用起來不方便
NSBlockOperation
1 //定義操作並添加到隊列 2 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ 3 [self operationAction:@"Block Operation"]; 4 }]; 5 //將操作添加到隊列 6 [self.myQueue addOperation:op];
設置操作的依賴關系(順序執行)
1 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ 2 NSLog(@"%@ - 下載圖片", [NSThread currentThread]); 3 }]; 4 NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ 5 NSLog(@"%@ - 添加圖片濾鏡", [NSThread currentThread]); 6 }]; 7 NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ 8 NSLog(@"%@ - 更新UI", [NSThread currentThread]); 9 }]; 10 [op2 addDependency:op1]; 11 [op3 addDependency:op2]; 12 [self.myQueue addOperation:op1]; 13 [self.myQueue addOperation:op2]; 14 [[NSOperationQueue mainQueue] addOperation:op3];
提示:利用addDependency可以指定操作之間的彼此依賴關系(執行先后順序)
注意:不要出現循環依賴!
NSOperationQueue還可以設置同時並發的線程數量
[self.myQueue setMaxConcurrentOperationCount:3];
設置同時並發的線程數量能夠有效地降低CPU和內存的開銷
這一功能用GCD不容易實現
Run Loop
(1) Run Loop提供了一種異步執行代碼的機制,不能並行執行任務
(2) 在主隊列中,Main Run Loop直接配合任務的執行,負責處理UI事件、計時器,以及其它內核相關事件
(3) Run Loop的主要目的是保證程序執行的線程不會被系統終止
Run Loop的工作特點
- 當有事件發生時,Run Loop會根據具體的事件類型通知應用程序做出響應
- 當沒有事件發生時,Run Loop會進入休眠狀態,從而達到省電的目的
- 當事件再次發生時,Run Loop會被重新喚醒,處理事件
主線程和其他線程中的Run Loop
- iOS程序的主線程默認已經配置好了Run Loop
- 其他線程默認情況下沒有設置Run Loop
- 一般在開發中很少會主動創建RunLoop,而通常會把事件添加到RunLoop中
多線程中的資源共享
並發編程中許多問題的根源就是在多線程中訪問共享資源。資源可以是一個屬性、一個對象、網絡設備或者一個文件等
在多線程中任何一個共享的資源都可能是一個潛在的沖突點,必須精心設計以防止這種沖突的發生
為了保證性能,atomic僅針對屬性的setter方法做了保護
而爭搶共享資源時,如果涉及到屬性的getter方法,可以使用互斥鎖@synchronized可以保證屬性在多個線程之間的讀寫都是安全的
無論是atomic還是@synchronized,使用的代價都是高昂的,不建議使用
建議:多線程是並發執行多個任務提高效率的,如果可能,應該在線程中避免爭搶共享資源
正是出於性能的考慮,UIKit中的絕大多數的類都不是線程安全的,因此,蘋果公司要求:更新UI相關的操作,應該在主線程中執行
單例模式
- 單例模式是一種常用的軟件設計模式
- 通過單例模式可以保證系統中一個類只有一個實例而且該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源
- 如果希望系統中某個類的對象只能存在一個,單例模式是最好的解決方案
- iOS中最常見的單例就是UIApplication
- 應用場景:
音頻播放,背景音樂!
硬件資源:加速器、[UIScreen mainScreen]
下面是實現單例模式的OC代碼
DemoObj.h
1 @interface DemoObj : NSObject 2 3 + (instancetype)sharedDemoObj; 4 5 @end
DemoObj.m
1 @implementation DemoObj 2 /** 3 1. 重寫allocWithZone,用dispatch_once實例化一個靜態變量(dispatch_once是線程安全的,能夠做到在多線程的環境下Block中的代碼只會被執行一次) 4 2. 寫一個+sharedXXX方便其他類調用 5 */ 6 // 在OC中,所有對象的內存空間的分配,最終都會調用allocWithZone方法 7 // 如果要做單例,需要重寫此方法 8 // GCD提供了一個方法,專門用來創建單例的 9 + (instancetype)allocWithZone:(struct _NSZone *)zone { 10 static DemoObj *result; 11 static dispatch_once_t onceToken; 12 dispatch_once(&onceToken, ^{ 13 result = [super allocWithZone:zone]; 14 }); 15 return result; 16 } 17 18 + (instancetype)sharedDemoObj { 19 return [[self alloc] init]; 20 } 21 @end
Swift代碼
class Singleton: NSObject {
internal class func sharedInstance() -> Singleton {
struct Static {
static var onceToken : dispatch_once_t = 0
static var instance : Singleton? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = Singleton()
}
return Static.instance!
}
}
單例小結
優點: 可以阻止其他對象實例化單例對象的副本,從而確保所有對象都訪問唯一實例
缺點: 單例對象一旦建立,對象指針是保存在靜態區的,單例對象在堆中分配的內存空間,會在應用程序終止后才會被釋放
提示: 只有確實需要唯一使用的對象才需要考慮單例模式,不要濫用單例
NSObject的多線程方法
- 開啟后台執行任務的方法
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
- 在后台線程中通知主線程執行任務的方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
- 獲取線程信息
[NSThread currentThread]
- 線程休眠
[NSThread sleepForTimeInterval:2.0f];
- 特點
使用簡單,量級輕
不能控制線程的數量以及執行順序
注意事項
NSObject的多線程方法使用的是NSThread的多線程技術
而NSThread的多線程技術不會自動使用@autoreleasepool
在使用NSObject或NSThread的多線程技術時,如果涉及到對象分配,需要手動添加@autoreleasepool
復習一下autoreleasepool
iOS開發中的內存管理
- 在iOS開發中,並沒有JAVA或C#中的垃圾回收機制
- 使用ARC開發,只是在編譯時,編譯器會根據代碼結構自動添加了retain、release和autorelease
自動釋放池的工作原理
- 標記為autorelease的對象在出了作用域范圍后,會被添加到最近一次創建的自動釋放池中
- 當自動釋放池被銷毀或耗盡時,會向自動釋放池中的所有對象發送release消息
每個線程都需要有@autoreleasepool,否則可能會出現內存泄漏,但是使用NSThread多線程技術,並不會為后台線程創建自動釋放池
