多線程詳解-NSThread、GCD、NSOperation

線程的串行

  • 一個線程中任務的執行是串行的
    • 如果要再一個線程中執行多個任務,那么只能一個一個的按順序執行這些任務
    • 也就是說同一時間內,一個線程只能執行一個任務

多線程

  • 一個進程中,可以開啟多條線程,每條線程可以并行(同時)執行不同的任務

多線程的優缺點

  • 優點:

    • 能適當提高程序的執行效率
    • 能適當提高資源利用率(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:^{
}];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容