目錄:
<a href="#(一)">(一)線程與進程之間的區別</a>
<a href="#(二)">(二)為什么需要學習多線程</a>
<a href="#(三)">(三)多線程任務執行方式</a>
<a href="#(四)">(四)多線程執行的原理</a>
<a href="#(五)">(五)多線程的優缺點</a>
<a href="#(六)">(六)在iOS開發中的多線程實現技術方案</a>
<li><a href="#(A)">(A)PThread</a>
<li><a href="#(B)">(B)NSThread</a>
<li><a href="#(C)">(C)GCD</a>
<ul><a href="#(1)">(1) dispatch_get_global_queue 探究</a></ul>
<ul><a href="#(2)">(2)dispatch_group的探索</a></ul>
<ul><a href="#(3)">(3)dispatch_once探究</a></ul>
<ul><a href="#(4)">(4)dispatch_after探究</a></ul></li>
<li><a href="#(D)">(D)NSOperation</a>
<ul><a href="#(D1)">(1)NSInvocationOperation探究</a></ul>
<ul><a href="#(D2)">(2)NSBlockOperation探究</a></ul>
<ul><a href="#(D3)">(3)NSOperationQueue探究</a></ul>
<ul><a href="#(D4)">(4)自定義NSOperation子類探究</a>
<ul><a href="#(D4.1)">(4.1)maxConcurrentOperationCount 屬性</a></ul>
<ul><a href="#(D4.2)">(4.2)addDependency 方法添加依賴:</a></ul></ul>
<a href="#(七)">(七)線程鎖相關</a></br>
<a href="#(八)">(八)總結</a></br></br>
【文章篇幅有點偏多,有興趣的可以繼續讀下去】
一般說到線程,那么首先要區分一下線程
與進程
,首先來簡單的區分一下兩者的關系
進程:是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。
線程:是指進程內的一個執行單元,也是進程內的可調度實體。是進程的一個實體,是CPU調度和分派的基本單位,他是比進程更小的能獨立運行的基本單位,線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(寄存器,棧,程序計數器),但是它可與同一個進程的其他線程共享進程所擁有的全部資源
<a name="(一)">(一)線程與進程之間的區別</a>
(1)地址空間:進程內的一個執行單元,進程至少包含一個線程,他們共享進程的地址空間,而進程有自己獨立的地址空間
(2)資源擁有:進程是資源分配和擁有的單位,同一個進程內的線程共享進程資源 【進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。】
(3)線程是處理器調度的基本單位,但進程不是
(4)二者皆可并發執行
<a name="(二)">(二)為什么需要學習多線程</a>
因為在程序運行中,對于網絡請求、圖片加載、文件處理、數據存儲、任務執行等等
這些操作都需要放到異步線程中進行處理,這也顯得多線程的重要性
<a name="(三)">(三)多線程任務執行方式</a>
主要分為兩種:串行
和并行
串行:(簡易)指的是多個任務按照一定順序執行(任務執行有順序依賴關系),例如有三個任務執行,并且需要的執行順序是 線程1->線程2->線程3
,那么這三個任務執行完畢所需的時間就是 t1 + t2 + t3
并行:(簡易)并發執行多個任務(任務執行沒有順序依賴關系),例如有三個任務執行,假設任務2的執行時間最長,那么這三個任務執行完畢所需的時間就是 t2
有一點需要明白的是:兩種任務執行方式并沒有好壞之分的,只是根據自己的需求進行選擇使用
并行執行
還是串行執行
<a name="(四)">(四)多線程執行的原理</a>
在單核操作系統的多線程執行,其實是采用時間片輪轉調度來實現的,操作系統會采用時間片輪轉調度的方式為每一個線程間接性的分配時間執行任務,當線程1執行的時候,線程2就處于阻塞或者空閑的狀態,當時間片執行到線程2時,執行循序有會反過來,所以對于單核操作系統來說的多線程執行方式就是:
宏觀上的并行,微觀上的串行
對于多核操作系統來說,就可以說是真正意義上的并行執行,因為每一個處理器都會按照時間片輪轉的方式執行任務,多個核心處理器就可以實現多個任務同時執行的效果
<a name="(五)">(五)多線程的優缺點</a>
優點:
(1)簡化了變成模型:可以將原本放在一個線程中執行的一些耗時或較為大的任務進行分割到多個線程中執行
(2)更加輕量級
(3)提高了執行效率
(4)提高資源利用率
缺點:
(1)增加了程序設計的復雜性:因為在多線程中我們需要處理的最大問題就是資源共享問題
和數據讀寫問題
,如果兩個線程同時修改同一個數據或屬性,就會出現問題,所以在一定程度上增加了程序設計的復雜性
(2)占用內存空間:因為如果不分場合隨意使用多線程的時候,會導致程序內存的增加,這對客戶端開發來說是一個絕對不能忽視的問題,所以我們需要適度、合理的使用多線程開發
(3)增加CPU調度開銷:因為在多線程執行任務時,是使用時間片調度的方式進行的,頻繁的切換時間片,必然會增大CPU的調度開銷
<a name="(六)">(六)在iOS開發中的多線程實現技術方案</a>
下面就通過Demo對這四種方式進行一一解釋
<a name="(A)">(A)PThread</a>
#pragma mark ---- 測試 pThread
/**
測試 pThread
*/
- (IBAction)runPThread:(id)sender {
NSLog(@"我是在主線程中執行\n\n");
pthread_t pthread;
pthread_create(&pthread, NULL, run, NULL);
}
/**
C語言函數
*/
void * run(void * data){
NSLog(@"我是在子線程中執行\n\n");
for (int i = 1; i <= 10; i++) {
NSLog(@"%d \n\n",i);
sleep(1);
}
return NULL;
}
從代碼中可以看出pThread的創建執行其實也是比較簡單的,不過實現過程是通過C語言進行的,從創建方法pthread_create(<#pthread_t _Nullable *restrict _Nonnull#>, <#const pthread_attr_t *restrict _Nullable#>, <#void * _Nullable (* _Nonnull)(void * _Nullable)#>, <#void *restrict _Nullable#>)
可以看出,第一個參數是需要一個pthread 對象指針,第三個是需要一個C語言函數方法(就當于OC中綁定的執行方法),至于第二個和第四個參數,暫時沒有什么用(其實偶也不曉得什么作用)可以直接傳入NULL
從打印結果中可以看出和我們預期的結果相同,成功的開啟了一個子線程
細心地童鞋可以會發現圖中紅色箭頭指向的兩組數字,其實在我們的輸出控制臺輸出的都有這兩組數字,但是很多朋友可能并沒有注意過這些,也不知道是什么意思?!
其實第一組數字
24592
表示的是當前程序所處的 進程 ID
,而第二組數字1923132
則表示當前所處的線程 ID
,所以我們就可以通過線程ID
進行判斷是否成功開啟了一個子線程
<a name="(B)">(B)NSThread</a>
NSThread可能是我們在OC開發中接觸最早的多線程實現技術,而且NSThread的實現多線程的方式也有三種,下面就通過代碼做解釋
NSThread的實現方式一:
#pragma mark ---- 測試 NSThread
/**
測試 NSThread
*/
- (IBAction)runNSThread:(id)sender {
NSLog(@"我是在主線程中執行\n");
/*
創建方式 1 :通過 alloc initWithTarget 進行創建
好處:可以通過 NSThread 對象設置一些線程屬性;例如線程 名字
*/
NSThread * thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(runThread1) object:nil];
[thread1 setName:@"Name_Thread1"];// 設置線程名字
[thread1 setThreadPriority:0.1];// 設置線程優先級
[thread1 start];
NSThread * thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(runThread1) object:nil];
[thread2 setName:@"Name_Thread2"];// 設置線程名字
[thread2 setThreadPriority:0.5];// 設置線程優先級
[thread2 start];
}
/// 方式一
-(void)runThread1{
for (int i = 11; i <= 20; i++) {
NSLog(@"%d -- %@",i,[NSThread currentThread].name);
sleep(1);
if (i == 20) {
[self performSelectorOnMainThread:@selector(runMainThread) withObject:nil waitUntilDone:YES];
}
}
}
-(void)runMainThread{
NSLog(@" 回調主線程");
}
方式一是通過 alloc initWithTarget 進行創建,這種方式的好處是可以通過 NSThread 對象設置一些線程屬性;例如線程 名字,從控制臺信息可以看出來,當設置了不同的NSThread對象的優先級屬性,可以控制其執行的順序,優先級越高,越先執行;而設置名字屬性后,可以通過調試監控當前所處線程,便于問題分析
NSThread的實現方式二:
// 創建方式 2 :通過 detachNewThreadSelector 方式創建并執行線程
[NSThread detachNewThreadSelector:@selector(runThread2) toTarget:self withObject:nil];
/// 方式二綁定方法
-(void)runThread2{
NSLog(@"我是在子線程中執行\n\n");
for (int i = 11; i <= 20; i++) {
NSLog(@"%d \n\n",i);
sleep(1);
if (i == 20) {
[self performSelectorOnMainThread:@selector(runMainThread) withObject:nil waitUntilDone:YES];
}
}
}
NSThread的實現方式三:
// 創建方式 3 :通過 performSelectorInBackground 方式創建并執行線程
[self performSelectorInBackground:@selector(runThread3) withObject:nil];
/// 方式三綁定方法
/// 方式三
-(void)runThread3{
NSLog(@"我是在子線程中執行\n");
for (int i = 21; i <= 30; i++) {
NSLog(@"%d \n",i);
sleep(1);
if (i == 30) {
[self performSelectorOnMainThread:@selector(runMainThread) withObject:nil waitUntilDone:YES];
}
}
}
在三組控制臺輸出結果對比可以發現,三種方式都能達到預期效果
<a name="(C)">(C)GCD</a>
關于GCD可能也是我們開發過程中使用最多的一種方式,但是大多數可能都只是只知其一,不知其二
,會用其中一兩個方法,就覺得會用GCD啦,其實這是遠遠不夠的,那我們就一起來探討一下GCD的強大之處:
1、GCD的描述:
純C語言開發,是蘋果公司為多核的并行運算提出的解決方案,會自動利用更多的CPU內核(比如雙核、四核),可以自動管理線程的生命周期(創建線程、調度任務、銷毀線程)。
2、GCD的兩個核心
2.1 任務
執行的操作,在GCD中,任務是通過 block來封裝的。并且任務的block沒有參數也沒有返回值。
2.2 隊列存放任務包括
串行隊列
并發隊列
主隊列
全局隊列
首先還是像上面一樣通過簡單Demo看看它的基本功能:
#pragma mark ---- 測試 GCD
- (IBAction)runGCD:(id)sender {
NSLog(@"執行 GCD");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@" start tast 1");
// 執行耗時任務
[NSThread sleepForTimeInterval:3];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回調主線程刷新UI");
});
});
}
打印結果:
同樣能夠實現這樣的功能,接下來就一步步的來具體分析GCD:
<a name="(1)">(1) dispatch_get_global_queue 探究:</a>
由打印信息可以看出,三個線程是同一時間開始執行,同一時間結束執行的,
這就說明GCD中的dispatch_get_global_queue是全局并發的隊列
/*
第一個參數設置隊列 優先級,這樣可以控制任務開始執行的先后順序,第二個參數沒有用到
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 高優先級
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默認
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) 低優先級
*/
dispatch_get_global_queue(long identifier, unsigned long flags)
這樣可以根據自己的需要控制任務開始執行的先后順序。但是如果想讓任務結束的時間也按照我們的意愿進行,那就需要使用到串行隊列,我們可以根據需要自定義串行隊列或者并行隊列
/*
自定義隊列 queue
參數一:隊列標識符
參數二:定義隊列是串行還是并行,NULL(默認)或者 DISPATCH_QUEUE_SERIAL 為串行,DISPATCH_QUEUE_CONCURRENT 表示并行隊列
*/
dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)
由上面的這列張圖所示的輸出信息可以清楚的看出自定義串行隊列和并行隊列的區別。
<a name="(2)">(2)dispatch_group的探索:</a>
隊列組就是可以對多個隊列進行操作的一個組,在隊列組中可以對不同隊列進行操作監聽結果等等,首先來說一下隊列組的監聽方法dispatch_group_notify
的用法:
NSLog(@"執行GCD");
dispatch_queue_t queue = dispatch_queue_create("GCD_Group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"start task 1");
[NSThread sleepForTimeInterval:2];
NSLog(@"end task 1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"start task 2");
[NSThread sleepForTimeInterval:2];
NSLog(@"end task 2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"start task 3");
[NSThread sleepForTimeInterval:2];
NSLog(@"end task 3");
});
/// group 組的監聽通知,所有task結束之后回調
dispatch_group_notify(group, queue, ^{
NSLog(@"All tasks over");
/*
并非另外開辟一個新線程,而是在三個任務中的其中一個子線程進行回調,
所以如果需要進行刷新 UI的話,需要回調到主線程處理
*/
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回調主線程刷新UI");
});
});
運行結果為:
由打印結果可以看出,將三個并行隊列放入到隊列組中時,使用
dispatch_group_notify
方法可以對隊列執行的結果進行監聽,而且這個監聽回調只有在隊列組中的三個異步線程都處理完成時才會執行回調,這在我們實際開發過程中也是一項非常常見的需求!不少童鞋看到這里可能覺得會用
dispatch_group_notify
隊列組了,但是還有一種更常見的情況是需要倍加注意的,具體請見下列demo:dispatch_group_notify
,現在打印的結果卻是:任務1和任務2開始之后,隊列組就回調了dispatch_group_notify
,頓時感覺自己使用了一個假的dispatch_group
隊列組......其實這才是實際開發中最常遇到的場景:當我們執行的任務中調起了一個異步的API請求,那么只要這個異步請求開始發送之后,
dispatch_group_async
就會認為當前任務已經處理完畢,之后這個異步API處理的事情就不在我的監控范圍之內啦,所以就造成了這種打印結果的出現。那么面對這種情況,需要如何處理才能正確監聽任務執行結果呢?如下處理:
dispatch_group_enter
和dispatch_group_leave
便可對隊列組中的不同異步請求進行監聽,最終執行回調dispatch_group_async
方法。但是有兩點需要注意的是:(1)dispatch_group_enter
和dispatch_group_leave
的使用必須是成對出現;(2)dispatch_group_leave
必須放在任務的最后一句執行當然GCD的隊列組的奧秘遠不止這些,目前只是列出了常用的集中以及使用場景,如果感興趣的大神可以繼續參考官方API研究!
<a name="(3)">(3)dispatch_once探究:</a>
dispatch_once
是GCD提供的一種創建單例的API方法,因為在我們的實際開發過程中,單例也是非常常用的一個場景,例如全局的數據、公共對象等等這些都需要通過單例進行處理,而單例顧名思義,就是在工程的整個運行過程中只會創建一次,然后會存在于內存中。例如:
/// 單例的創建
+(instancetype)instance {
static dispatch_once_t onceToken;
static SingleTest * inst = nil;
dispatch_once(&onceToken, ^{
NSLog(@"初始化單例對象");
inst = [[SingleTest alloc]init];
});
return inst;
}
調用方法
從輸出也可以看出來,只有當第一次點擊方法時會創建對象,之后點擊方法時將不會在次創建對象,所有打印的對象內存地址都相同,證明是同一個單例對象
<a name="(4)">(4)dispatch_after探究:</a>
這是GCD中提供的一個延時操作API,使用起來很簡單,但是在個方法會存在一個陷阱,當延時操作開始之后將無法取消,所以當在一個界面執行延時操作時,界面消失之后仍然會執行操作,這樣就可能造成程序crash,所以使用的時候需要多加注意。以上就是對GCD進行的一個簡單了解
<a name="(D)">(D)NSOperation</a>
1、NSOperation簡介
1.1 NSOperation與GCD的區別:
OC語言中基于 GCD 的面向對象的封裝;
使用起來比 GCD 更加簡單;
提供了一些用 GCD 不好實現的功能;
蘋果推薦使用,使用 NSOperation 程序員不用關心線程的生命周期
1.2 NSOperation的特點
NSOperation 是一個抽象類,抽象類不能直接使用,必須使用它的子類
抽象類的用處是定義子類共有的屬性和方法
2、核心概念
將操作添加到隊列,異步執行。相對于GCD創建任務,將任務添加到隊列。
將NSOperation添加到NSOperationQueue就可以實現多線程編程
3、操作步驟
先將需要執行的操作封裝到一個NSOperation對象中
然后將NSOperation對象添加到NSOperationQueue中
系統會自動將NSOperationQueue中的NSOperation取出來
將取出的NSOperation封裝的操作放到一條新線程中執行
<a name="(D1)">(1)NSInvocationOperation探究</a>
1、從打印輸出的線程 ID可以看出:NSInvocationOperation的輸出操作和
[invocationOper start]
是在同一個線程中,即[invocationOper start]
如果在主線程中發起,則NSInvocationOperation的輸出操作也在主線程;[invocationOper start]
如果在子線程中發起,則NSInvocationOperation的輸出操作也在相應的子線程中;NSInvocationOperation不會開啟一個新線程2、有打印輸出的順序可以看出:NSInvocationOperation的執行是同步執行的
<a name="(D2)">(2)NSBlockOperation探究</a>
NSBlockOperation
打印的結果和上面NSInvocationOperation
如出一轍,一毛一樣,這也就證明了系統提供的兩個子類NSInvocationOperation ``NSBlockOperation
都是同步執行的
<a name="(D3)">(3)NSOperationQueue探究</a>
首先來看一下其相關的概念及關鍵詞
在使用
NSOperationQueue
對象addOperation
的方式執行任務,而不是通過 start
執行,輸出打印的結果會有明顯的不同1、
NSOperationQueue
執行任務會開啟一個新線程2、
NSOperationQueue
執行任務是一個異步的操作過程
<a name="(D4)">(4)自定義NSOperation子類探究 </a>
首先我們可以創建一個NSOperation
的子類,并且重寫main
方法,在代碼中是一個什么效果呢?
<a name="(D4.1)">(4.1)maxConcurrentOperationCount 屬性: </a>
未設置并發數時,默認所有任務同時并發執行
當設置了最大并發數為 2 時,如下圖可以看出
NSOperationQueue
同時執行的任務數也為兩個,當前兩個任務執行完畢之后才繼續執行后面的任務<a name="(D4.2)">(4.2)addDependency 方法添加依賴: </a>
一般在我們的實際開發過程中,會遇到異步任務一需要等待異步任務二完成之后才能執行,這種情況下可以就會想到使用多線程的依賴進行實現(當然使用上面說的GCD也可以),那下面就說一下 NSOperation
中的addDependency
方法:
[customA addDependency:customC];
[customC addDependency:customB];
[customB addDependency:customD];
這三句表示的依賴關系是:customA -> customC -> customB -> customD``customA任務
需要在customC任務
執行之后才能執行;customC任務
需要在customB任務
執行之后才能執行;customB任務
需要在customD任務
執行之后才能執行【注意:customD任務
不能再依賴于customA任務
,否則就會造成死鎖】;使用看到了最終控制臺輸出的順序效果。
當然上面這是一種理想的狀態,如果出現了下面這種 “ 變態 ” 情況,這種依賴關系還可靠嗎???
當自定NSOperation的自定義類中的main
方法執行的是一個異步任務:
-(void)main{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1];
if (self.cancelled) {
return ;
}
NSLog(@"---%@",self.operName);
});
}
輸出的打印順序如下:
其實是因為自定義的NSOperation子類
main
方法中,因為main
方法執行的是一個異步任務,當任務開始執行之后,NSOperation子類就默認依賴任務完成,而無法監聽到這個異步任務執行結束。但是這種場景也是實際開發中經常用到的,所以要怎樣處理呢?解決方法就是使用
NSRunLoop
進行解決:while (!self.over && !self.cancelled) {
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
在代碼中的這句作用就是讓當前的 RunLoop 在main方法中
等待異步任務的結束,這樣一來問題就完美解決啦,下面看一下輸出效果:
輸出的結果符合自己設置的依賴預期,問題完美解決。
<a name="(七)">(七)線程鎖相關</a>
多線程在開發中給我們帶來了很多遍歷,但是正如上面所說的多線程也存在一些缺點,例如:
統一個資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源,當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題,那么這里就需要強調一下線程鎖
的概念:
關于線程鎖的說明,有一個最經典的例子就是購票系統的例子:下面我也根據這個場景說明一下線程鎖的使用及重要性:
#import "TicketManager.h"
@interface TicketManager ()
/**
剩余票數
*/
@property (nonatomic,assign)NSInteger tickets;
/**
賣出票數
*/
@property (nonatomic,assign)NSInteger saleCount;
/**
杭州賣票點(線程模擬)
*/
@property (nonatomic,strong)NSThread * thread_HZ;
/**
上海買票點(線程模擬)
*/
@property (nonatomic,strong)NSThread * thread_SH;
@end
#define TotalTicket 10// 總票數
@implementation TicketManager
- (instancetype)init{
if (self = [super init]) {
self.tickets = TotalTicket;
self.thread_HZ = [[NSThread alloc]initWithTarget:self selector:@selector(sale) object:nil];
[self.thread_HZ setName:@"HZ_Thread"];
self.thread_SH = [[NSThread alloc]initWithTarget:self selector:@selector(sale) object:nil];
[self.thread_SH setName:@"SH_Thread"];
}
return self;
}
/// 訪問同一份資源,票庫
-(void)sale {
while (true) {
if (self.tickets > 0) {
[NSThread sleepForTimeInterval:0.5];
self.tickets -- ;
self.saleCount = TotalTicket - self.tickets;
NSLog(@"站點:%@, 當前余票:%ld,售出:%ld",[NSThread currentThread].name,(long)self.tickets,(long)self.saleCount);
}
}
}
/// 開始賣票
-(void)startToSaleTicket{
[self.thread_HZ start];
[self.thread_SH start];
}
@end
這種是一個沒有線程鎖的情況,那先看一下打印的輸出結果:
從結果可以明顯的看出多線程訪問統一資源的問題,會出現數據錯亂。接下來就看一下幾種線程鎖:
1、互斥鎖@synchronized (self) 【使用簡單,但是小號CPU資源較大】
2、NSCondition加鎖
3、NSLock加鎖
從三種加鎖方式的輸出結果可以看出,都能達到預期,能有效防止因多線程搶奪資源造成的數據安全問題。至于具體使用哪種方式,可以根據自己的需求進行選擇。
<a name="(八)">(八)總結</a>
本文只是對多線程進行了一個簡單的探索研究,希望能夠幫助到有需要的童鞋,文章提到的一些知識點并不是很深,需要進行深入研究的朋友可以直接翻看官方API,如果文章中有不足的地方,歡迎指正!