線程的串行
- 一個線程中任務的執行是串行的
- 如果要再一個線程中執行多個任務,那么只能一個一個的按順序執行這些任務
- 也就是說同一時間內,一個線程只能執行一個任務
多線程
- 一個進程中,可以開啟多條線程,每條線程可以并行(同時)執行不同的任務
多線程的優缺點
-
優點:
- 能適當提高程序的執行效率
- 能適當提高資源利用率(CPU、內存利用率)
-
缺點:
- 創建線程是有開銷的,iOS下主要成本:內核數據結構(大于1KB)、棧空間(子線程512KB、主線程1MB),創建線程大于需要90毫秒的創建時間
- 如果開啟大量線程,會降低程序的性能
- 線程越多,CPU在調度線程上的開銷就越大
- 程序設計更加復雜:比如線程之間的通信、多線程的數據共享
iOS中多線程的實現方案
-
pthread:
- 一套C語言的通用的多線程API
- 跨平臺,使用難度大
- 程序員管理線程的生命周期,在iOS開發中幾乎不用
-
NSThread:
- 一套OC的多線程API
- 使用更加面向對象,簡單,更容易操作線程對象
- 程序員管理生命周期,在iOS開發中偶爾使用
-
GCD:
- 一套C語言的多線程API
- 旨在替代NSThread等線程技術,充分利用設備的多核
- 系統自動管理生命周期,在iOS開發中
經常使用
-
NSOperation:
- 一套OC的對象稱API
- 基于GCD(底層是GCD)
- 比GCD多了一些簡單實用的功能
- 系統自動管理生命周期,在iOS開發中
經常使用
pthread(了解)
// 創建pthread
pthread_t thread;
pthread_create(&thread, NULL, method, NULL);
void * method(void *param){
// 執行耗時操作
return NULL;
}
NSThread
// 創建線程
// 使用NSThread創建的線程去執行耗時操作,當耗時操作執行完,線程會自動回收
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// 啟動線程
[thread start];
// 獲得主線程
[NSThread mainThread];
// 是否是主線程
[thread isMainThread];
[NSThread isMainThread];
// 獲得當前線程
[NSThread currentThread];
// 設置創建的線程的名字
[thread setName:@"11"];
// 其他創建線程的方式
// 創建線程后自動啟動線程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
// 隱式創建并啟動線程
[self performSelectorInBackground:@selector(run) withObject:nil];
-
線程的狀態
-
新建(New)
:調用start方法啟動,進入就緒(Runnable)狀態
- CPU調度當前線程,進入
運行(Running)狀態
- CPU調度其他線程,進入
就緒(Runnable)狀態
- 調用sleep方法或者等待同步鎖,進入
阻塞(Blocked)狀態
- sleep時間到或者得到同步鎖,進入
就緒(Runnable)狀態
- 線程任務執行完畢、異常、強制退出,進入
死亡(Dead)狀態
-
控制線程狀態
// 啟動線程
// 進入就緒狀態 -> 運行狀態,當前任務執行完畢,自動進入死亡狀態
[thread start];
// 阻塞(暫停)線程
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[NSThread sleepForTimeInterval:11];
// 強制停止線程
// 進入死亡狀態,線程一旦死亡,就不能再次開始任務
[NSThread exit];
- 多線程的安全隱患
- 多個線程可能
訪問同一塊資源
,這樣很容易引起數據錯亂和數據安全
問題 - 解決辦法:互斥鎖(@synchronized)
- 鎖定1份代碼只用一把鎖,用多了無效
- 優點:能有效防止因多線程搶奪資源造成的數據安全問題
- 缺點:需要消耗大量的CPU資源
- 使用場景:多條線程搶奪同一塊資源,比如:修改同一個數據、修改同一個文件等
- 多個線程可能
- (void)run
{
// 執行耗時操作
@synchronized (self) {
// 需要執行的代碼
}
}
- 線程間通訊
- 一個線程傳遞數據給另一個線程
- 一個線程執行完特定任務之后,轉到另一個線程繼續執行任務
// waitUntilDone:是否等待run方法執行完成之后才繼續執行
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
[self performSelector:@selector(run) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO];
GCD(掌握,開發中經常使用)
- 純C語言,提供了很多非常強大的函數
- GCD的優勢:
- GCD是蘋果公司為多核的并行運算提出的解決方案
- GCD會自動利用更多的CPU內核
- GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
- 程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼
任務和隊列(這兩個是GCD的核心概念)
- 任務:執行什么操作
- 隊列:用來存放任務
GCD使用的步驟
- 定制任務
- 將任務添加到隊列中
- GCD會自動將隊列中的任務取出,放到對應的線程中執行
- 任務的取出遵循隊列的FIFO原則:
先進先出,后進后出
GCD中用來執行任務的常用函數
- 同步執行任務:
dispatch_sync
- 異步執行任務:
dispatch_async
- 同步和異步的區別:
- 同步:只能在
當前線程
中執行任務,不具備開啟新線程的能力
- 異步:
可以在新的線程
中執行任務,具備開啟新線程的能力
- 同步:只能在
// 用異步的方式執行任務
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
// 用同步的方式執行任務
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
隊列的類型
-
并發隊列
- 可以讓多個任務并發(同時)執行(自動開啟多個線程同時執行任務)
- 并發功能只有在
異步函數(dispatch_async)
下才有效
-
串行隊列
- 讓任務
一個接著而一個
的去執行(一個任務執行完畢后,在執行下一個任務)
- 讓任務
-
容易混淆的術語
-
同步
和異步
主要影響:能不能開啟新線程
- 同步:只是在當前線程中執行任務,不具備開啟新線程的能力
- 異步:可以在新的線程中執行任務,具備開啟新線程的能力
-
并發
和串行
主要影響:任務的執行方式
- 并發:允許多個任務并發執行
- 串行:任務一個接著一個的執行
-
并發隊列
- 使用
dispatch_queue_create
函數創建即可
// "com.eyee.queue" : 表示隊列的名稱
// DISPATCH_QUEUE_CONCURRENT : 表示隊列的類型是并發隊列
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_CONCURRENT);
- 獲取全局的并發隊列
// DISPATCH_QUEUE_PRIORITY_DEFAULT : 表示隊列的優先級
// 第二個參數暫時無用,保留參數,傳0即可
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 全局并發隊列的優先級
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 中(默認)
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺
串行隊列
- 使用
dispatch_queue_create
函數創建
// "com.eyee.queue" : 表示隊列的名稱
// DISPATCH_QUEUE_SERIAL : 表示隊列的類型是串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_SERIAL);
- 使用主隊列
- 主隊列是GCD自帶的一種特殊的串行隊列
- 放在主隊列中的任務,都會放到主線程中執行
- 使用dispatch_get_main_queue()獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
各種情況介紹
異步函數 + 并發隊列:可以同時開啟多條線程,并發執行任務
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
});
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
同步函數 + 并發隊列:不會開啟新的線程,在當前線程中串行執行任務
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
異步函數 + 串行隊列:會開啟新線程,但是任務是串行執行
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
});
dispatch_async(queue, ^{
});
dispatch_async(queue, ^{
});
同步函數 + 串行隊列:不會會開啟新線程,任務是串行執行
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
異步函數 + 主隊列:在主線程中串行執行任務
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
});
dispatch_async(queue, ^{
});
dispatch_async(queue, ^{
});
同步函數 + 主隊列
// 這種方式分兩種情況:
// 如果當前線程是主線程,那么程序會卡死,任務也不會執行
// 如果當前線程不是主線程,那么會在主線程中串行執行任務
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
dispatch_sync(queue, ^{
});
線程直接通信
- 最常用的就是在子線程中執行耗時操作,執行完畢之后回到主線程刷新UI
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 執行耗時操作
// 回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI
});
});
其他常用方法
- 執行任務方法:
dispatch_barrier_async
在前面的任務執行結束之后它才執行,而且它后面的任務需要等到它執行完畢之后才會執行
這個queue不能是全局并發隊列
dispatch_queue_t queue = dispatch_queue_create("com.eyee.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
});
dispatch_async(queue, ^{
});
dispatch_barrier_async(queue, ^{
});
dispatch_async(queue, ^{
});
- 延時執行
[self performSelector:@selector(method) withObject:nil afterDelay:10];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(method) userInfo:nil repeats:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 10秒以后要執行的代碼
});
- 在程序生命周期內只執行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
- 快速迭代
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 第一個參數:循環多少次
// 第二個參數:執行任務的隊列
// 第三個參數:每次循環當前的index,相當于for循環中的i
dispatch_apply(10, queue, ^(size_t index) {
});
- 隊列組
- 執行兩個耗時操作
- 等兩個耗時操作執行完畢之后,在回到主線程執行操作
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 第一個耗時操作
});
dispatch_group_async(group, queue, ^{
// 第二個耗時操作
});
dispatch_group_notify(group, queue, ^{
// 1、執行操作
// 2、回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
});
});
NSOperation
-
NSOperation是一個抽象類,并不具備封裝操作的能力,必須使用它的子類:
NSInvocationOperation
NSBlockOperation
- 自定義子類
繼承NSOperation
,實現內部相應的方法
NSOperation和NSOperationQueue配合也能實現多線程變成
-
實現步驟:
- 將需要執行的操作封裝到一個NSOperation對象中
- 然后將NSOperation對象添加到一個NSOperationQueue鐘
- 系統會自動將NSOperationQueue中的NSOperation取出來
- 將取出的NSOperation封裝的操作放到一條新線程中執行
基本使用
- 使用NSBlockOperation注意點:
- 如果封裝的操作數是1個,那么就會在當前線程中執行
- 如果封裝的操作數大于1個,那么會不會開啟新的線程就要看系統的分配了(有的地方說會開啟新的線程,但是我測試發現有時候開啟,有時候不開啟)
// 一般情況下,調用start方法后并不會開啟新的線程
// 會在當前線程中執行操作
// 只有NSOperation放到NSOperationQueue中,才會開啟線程
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[op start];
// 創建NSBlockOperation對象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
}];
// 通過addExecutionBlock: 方法添加更多操作
[op addExecutionBlock:^{
}];
[op addExecutionBlock:^{
}];
[op start];
NSOperationQueue的作用
- NSOperation可以調用start方法來執行任務,但是默認是同步執行的
- 如果將NSOperation添加到NSOperationQueue中,系統自動異步執行NSOperation中的操作
- NSOperationQueue隊列分兩種:
- 主隊列:[NSOperationQueue mainQueue]
- 其他隊列:[[NSOperationQueue alloc] init]
- 這個是并發也是串行,靠設置最大并發數決定
// 創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 創建操作(任務)
// 創建NSInvocationOperation
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
// 添加任務到隊列中
// 第一種方式:
[queue addOperation:op]; // [op start]
// 第二種方式:
[queue addOperationWithBlock:^{
NSLog(@"download2 --- %@", [NSThread currentThread]);
}];
設置最大并發數
- 設置最大并發數之后,最多就開啟這么多條線程,如果設置為1,那么就是串行隊列
- 如果不設置,那么就讓系統自動開啟線程
// 設置最大并發操作數
// queue.maxConcurrentOperationCount = 2; // 并發隊列
queue.maxConcurrentOperationCount = 1; // 就變成了串行隊列
掛起隊列
- 有的時候需要暫停隊列,有的時候需要恢復隊列
- 滿足這個條件的屬性
suspended
- 注意點:
- 隊列中可能加入很多個操作,也可以設置最大并發數
- 如果隊列中的某個操作
已經開始執行
,那么暫停隊列,這個操作不會停下來
- 如果某個操作沒有開始執行,那么暫停隊列,這個操作不會執行,當恢復隊列之后繼續執行
if (self.queue.isSuspended) {
// 恢復隊列,繼續執行
self.queue.suspended = NO;
} else {
// 暫停(掛起)隊列,暫停執行
self.queue.suspended = YES;
}
取消操作
- 如果我們需要取消隊列中的操作,需要用到的方法就是
cancelAllOperations
- 注意點:
取消之后不能恢復
只能取消未開始的操作,已經開始的操作將繼續執行
- NSOperation中有cancel方法和cancelled屬性,隊列的cancelAllOperations方法也就是調用所有操作的cancel方法
// 這個方法是取消隊列中的所有操作
// 注意:這里是取消未開始的的操作
[self.queue cancelAllOperations];
-
當我們自定義NSOperation的時候
- 繼承自NSOperation
實現main方法,里面實現耗時操作
- 在main方法中如果有好幾個耗時操作,那么在每一個耗時操作開始之前,需要先判斷當前任務是否取消
自己創建自動釋放池
if (self.isCancelled) return;
操作依賴
- 比如一定要A操作執行完成之后才能執行B操作,那么可以使用依賴
- 注意點:
一定不能相互依賴
// op3 依賴 op1 和 op2
// 也就是op1 和 op2 執行完成之后,op3才會執行
[op3 addDependency:op1];
[op3 addDependency:op2];
操作監聽
- 監聽一個操作執行完畢
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
}];
op.completionBlock = ^{
// 操作執行完畢
};
線程間通信
- 基本上是回到主線程
- 在需要回到主線程的操作中,加入下面的代碼:
// 回到主線程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];