什么是進程?
-
進程
是指在系統中正在運行的一個應用程序。 - 每個進程之間是
獨立的
,每個進程均運行在其專用且受保護
的內存空間內。
什么是線程?
- 1個進程要想執行任務,
必須
得有線程(每1個進程至少要有1條線程
)。 - 線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行。
小拓展
:
- 線程的串行(就像烤串一樣)
- 1個線程中任務的執行是串行的。
- 如果要在1個線程中執行多個任務,那么只能一個一個地按順序執行這些任務。
- 在`同一時間內`,1個線程只能執行1個任務。
什么是多線程?
1個進程中可以開啟多條線程,每條線程可以并行(同時)執行不同的任務。
-
線程的并行(同時執行)
- 比如同時開啟3條線程分別下載3個文件(分別是文件A、文件B、文件C。
-
多線程并發執行的原理:
- 在同一時間里,CPU只能處理1條線程,只有1條線程在工作(執行)。
- 多線程并發(同時)執行,其實是CPU快速地在多條線程之間調度(切換),如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象。(如下圖)
CPU調用線程
多線程優缺點:
- 優點
- 能適當提高程序的執行效率。
- 能適當提高資源利用率(CPU、內存利用率)
- 缺點
- 開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能。
- 線程越多,CPU在調度線程上的開銷就越大。
- 程序設計更加復雜:比如線程之間的通信、多線程的數據共享
多線程在iOS開發中的應用
- 主線程
- 一個iOS程序運行后,默認會在自己的進程中開啟1條線程,稱為“主線程”也叫“UI線程”。
- 作用:刷新顯示UI,處理UI事件。
- 使用注意
- 不要將耗時操作放到主線程中去處理,因為會卡住主線程,造成UI卡頓(用戶體驗差)。
- 和UI相關的刷新操作`必須`放到主線程中進行處理。
線程的狀態
- 線程的各種狀態:新建-就緒-運行-阻塞-死亡
- 常用的控制線程狀態的方法
[NSThread exit];//退出當前線程 [NSThread sleepForTimeInterval:7.0];//阻塞線程 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:7.0]];//阻塞線程
注意:線程死亡后不能復生
線程安全:
- 前提:多個線程同時訪問同一塊資源會發生數據安全問題
解決方案
:加互斥鎖 - 相關代碼:@synchronized(self){}
- 專業術語-線程同步
- 原子和非原子屬性(是否對setter方法加鎖)
IOS中多線程的實現方案
方案 | 簡介 | 語言 | 線程生命周期 | 使用頻率 |
---|---|---|---|---|
pthread | 一套通用的多線程API (跨平臺\可移植) |
C語言 | 程序員管理 | 幾乎不用 |
NSThread |
使用更加面向對象 (簡單易用,可直接操作線程對象) |
OC語言 | 程序員管理 | 偶爾使用 |
GCD |
為了替代NSThread為生 ( 充分利用設備多核 ) |
C語言 | 系統自動管理 | 經常使用 |
NSOperation |
基于GCD ( 更加面向對象 更方便地設置線程之間的依賴 監聽線程狀態KVO ) |
OC語言 | 系統自動管理 | 經常使用 |
pthread簡單使用
1.包含頭文件(必須)
#import <pthread.h>
2.創建線程
// 創建線程
/**
*
* 參數一:線程對象(傳地址)
* 參數二:線程的屬性(名稱\優先級)
* 參數三:只想函數的指針
* 參數四:函數需要接受的字符串參數,可以不傳遞(注:由于我們創建的是OC的字符串,所以在傳值的時候需要將其轉換成C的字符串)
*/
pthread_t thread;
NSString *num = @"123";
pthread_create(&thread, NULL, task, (__bridge void *)(num));
3.定義參數所需要的函數指針
void *task(void *num)
{
NSLog(@"當前線程 -- %@,傳入的參數:-- %@", [NSThread currentThread], num);
return NULL;
}
如果需要退出線程的話只需調用下面代碼
pthread_exit(NULL);
運行結果:
NSThread簡單使用
這邊介紹NSThread創建線程的4種方式:
- 第一種 (alloc nitWithTarget:selector:object:)
- 特點:需要手動開啟線程,可以拿到線程對象進行詳細設置
- 優缺點:
- 缺點:需要手動開啟線程執行任務
- 優點:可以拿到線程對象
// 創建線程
/**
* 參數一:目標對象
* 參數二:方法選擇器(線程啟動后調用的方法)
* 參數三:調用方法需要接受的參數
*/
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(task)
object:nil];
// 開始執行
[thread start];
- 第二種(分離出一條子線程)
- 特點:自動啟動線程,無法對線程進行更詳細的設置
- 優缺點:
- 缺點:無法拿到線程對象 進行更詳細設置
- 優點:代碼簡單且自動開啟線程執行
// 創建線程
/**
* 參數一:要調用的方法
* 參數二:目標對象 self
* 參數三:調用方法需傳遞的參數
*/
[NSThread detachNewThreadSelector:@selector(task)
toTarget:self
withObject:nil];
- 第三種(后臺線程)
- 特點:自動啟動線程,無法進行更詳細設置
- 優缺點:
- 缺點:無法拿到線程對象 進行更詳細設置
- 優點:代碼簡單且自動開啟線程執行
/**
* NSThread創建一條后臺線程
*/
- (void)nsthreadTest3
{
// 創建線程
/**
* 參數一:要調用的方法
* 參數二:調用方法需傳遞的參數
*/
[self performSelectorInBackground:@selector(run:) withObject:@"后臺線程"];
}
- (void)run:(NSString *)str
{
NSLog(@"當前線程:%@ -- 接收到的參數:%@", [NSThread currentThread], str);
}
- 第四種(自定義NSThread類并重寫內部的方法實現)
- 特點:可以不暴露一些實現細節,使代碼增加隱蔽性。(一般出現在第三方框架內)
- 優缺點:
- 缺點:繁瑣,且需要手動開啟線程執行
- 優點:增加代碼隱蔽性
1.創建自定義類繼承自NSThread
2.重寫NSThread類中的main
方法
- (void)main
{
NSLog(@"當前線程--%@", [NSThread currentThread]);
}
3.創建線程對象
/**
* NSThread創建一條后臺線程
*/
- (void)nsthreadTest4
{
// 創建線程
SJThread *thread = [[SJThread alloc] init];
// 開啟執行
[thread start];
}
線程間通信
有時候我們會從服務器上下載圖片然后再展示出來,下載的操作我們會放到子線程,而UI刷新的操作只能在主線程中執行
。這樣就涉及到線程間的通信
。接下來我們分三種方式來簡單實現一下:
- 方式一:
- (void)viewDidLoad {
[super viewDidLoad];
// 開啟一條線程下載圖片
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
- (void)downloadImage
{
// 網絡圖片url
NSURL *url = [NSURL URLWithString:@"http://img3.imgtn.bdimg.com/it/u=3841157212,2135341815&fm=206&gp=0.jpg"];
// 根據url下載圖片數據到本地
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 把下載到本地的二進制數據轉成圖片
UIImage *image = [UIImage imageWithData:imageData];
// 回到主線程刷新UI
// 第一種方式
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
// 第二種方式
// 直接調用iconView里面的setImage:方法就可以實現刷新
// [self.iconView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
// 第三種方式
// 此方法可以方便自由在主線程和其它線程切換
// [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
- (void)showImage:(UIImage *)image
{
self.iconView.image = image;
}
GCD簡單使用
什么是GCD
- GCD全稱是Grand Central Dispatch(牛逼的中樞調度器)
- 純C語言,提供了非常多強大的函數
GCD優勢
- GCD是蘋果公司為多核的并行運算提出的解決方案
- GCD會自動利用更多的CPU內核
- GCD會自動關了線程生命周期(創建、調度、銷毀線程)
- GCD性能很好(接近底層)
GCD的組合方式
- 異步函數+并發隊列:開啟多條線程,并發執行任務
- 異步函數+串行隊列:開啟一條線程,串行執行任務
- 同步函數+并發隊列:不開線程,串行執行任務
- 同步函數+串行隊列:不開線程,串行執行任務
- 異步函數+主隊列:不開線程,在主線程中串行執行任務
- 同步函數+主隊列:不開線程,串行執行任務(注意死鎖發生
注意同步函數和異步函數在執行順序上面的差異
GCD的任務和隊列
- 任務:執行什么操作
- 隊列:用來存放任務(GCD中提供了2種隊列)
- 串行隊列
- 并發隊列
GCD的使用
- 定制任務 —— 確定需要做的操作
- 將任務添加到隊列中
- GCD會自動將隊列中的任務取出,存放到線程中執行
- 任務的取出遵循隊列的FIFO原則(先進先出,后進后出)
GCD創建線程
- 接下來看看同步函數和異步函數有什么區別:
1.先來看看異步并發隊列
- (void)test
{
/**
* 參數一:C語言的字符串,給隊列起一個名字或標識
* 參數二:隊列類型
DISPATCH_QUEUE_CONCURRENT 并發
DISPATCH_QUEUE_SERIAL 串行
*/
dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);
/**
* 使用函數封裝任務
* 參數一:獲取隊列
* 參數二:需要執行的任務
*/
dispatch_async(queue, ^{
NSLog(@"在:%@線程執行了任務",[NSThread currentThread]);
});
NSLog(@"結束");
}
執行結果:
2.再來看看同步并發隊列
- (void)test
{
/**
* 參數一:C語言的字符串,給隊列起一個名字或標識
* 參數二:隊列類型
DISPATCH_QUEUE_CONCURRENT 并發
DISPATCH_QUEUE_SERIAL 串行(串行隊列可以用NULL表示)
*/
dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);
/**
* 使用函數封裝任務
* 參數一:獲取隊列
* 參數二:需要執行的任務
*/
dispatch_sync(queue, ^{
NSLog(@"在:%@線程執行了任務",[NSThread currentThread]);
});
NSLog(@"結束");
}
執行結果:
結論:
從上面的2個運行結果的時間可以看出
1.異步并發隊列,會開啟一條子線程來處理任務,以達到主線程和子線程同時執行的并發效果。
2.同步并發隊列,不會開線程,必須等block塊中的代碼先執行完畢才會繼續執行以外的任務,所以并發隊列對于同步函數來說等同于“無效”
- 再看看并發隊列對異步函數和同步函數的影響:
1.同步函數+并發隊列
dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
執行結果:同步函數+并發隊列沒有開啟子線程的能力
2.異步函數+并發隊列
- (void)test2
{
dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
}
執行結果:異步函數+并發隊列會自動開啟3條子線程執行任務
結論:
從上面可以看出,異步函數擁有開啟子線程的能力,而同步函數沒有開啟子線程的能力。
- GCD中,除了并發隊列外,還有串行隊列,我們來看看如果把并發隊列換成串行隊列會有怎樣的變化
1.同步函數+串行隊列
- (void)test2
{
dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
}
執行結果:進一步證明同步函數沒有開啟子線程的能力,他的所有任務都在主線程中執行
2.異步函數+串行隊列
- (void)test2
{
dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@",[NSThread currentThread]);
});
}
執行結果:開啟了一條子線程,在子線程中依次執行任務
結論
1.在同步函數+串行隊列中,任務依舊是在主線程中執行。
2.在異步函數+串行隊列中,會自動開啟一條子線程,在子線程中依次執行任務
3.再一次證明同步函數沒有開啟子線程的能力
系統提供的4個全局并發隊列
- 在iOS中系統默認給我們提供了4個全局并發隊列
- (void)test3
{
// 獲取全局并發隊列
// 系統內部默認提供4個全局并發隊列
/**
* 參數一:優先級
* 參數二:時間(傳0即可)
*/
//優先級:DISPATCH_QUEUE_PRIORITY_HIGH 2
// DISPATCH_QUEUE_PRIORITY_DEFAULT 0
// DISPATCH_QUEUE_PRIORITY_LOW (-2)
// DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 級別最低
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"1當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"4當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"5當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"6當前線程:%@", [NSThread currentThread]);
});
}
執行結果:在結果中我們看到GCD創建了6條線程,但是實際上GCD創建多少條線程完全由系統當前情況而定,我們是無法控制的。
特殊的串行隊列 —— 主隊列(與主線程相關聯的隊列)
- 主隊列是GCD自帶的一種特殊的串行隊列
- 放在主隊列中的人物,都會放到主線程中執行
- 使用dispatch_get_main_queue()的方式可獲取主隊列
-
特點
- 1.放在主隊列中的任務,必須在主線程中執行
- 2.主隊列執行任務的時候,在調度任務的時候,會先調用主線程的狀態,如果當前有任務在做,則會等待主線程執行完任務再執行自己的任務
-
1.主隊列+異步函數
- (void)test4
{
// 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 添加任務
dispatch_async(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
}
執行結果:任務都在主線程中執行
2.同步函數+主隊列
- (void)test4
{
// 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 添加任務
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
}
執行結果:
進入死鎖狀態,因為主隊列執行任務的時候,在調度任務的時候,會先調用主線程的狀態,如果當前有任務在做,則會等待主線程執行完任務再執行自己的任務
如果要解決以上的情況,那么可以將任務添加到子線程中,這樣就不會出現死鎖的情況,程序也就能夠正常執行了
[self performSelectorInBackground:@selector(test4) withObject:nil];
- (void)test4
{
// 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 添加任務
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"當前線程:%@", [NSThread currentThread]);
});
}
執行結果:
總結
函數類型 | 并發隊列 | 手動創建的串行隊列 | 主隊列 |
---|---|---|---|
同步 (sync) | 1.沒有開啟新線程 2.串行執行任務 |
1.有開啟新線程 2.串行執行任務 |
死鎖 |
異步(async) | 1.有開啟新線程 2.并發執行任務 |
1.有開啟新線程 2.串行執行任務 |
1.沒有開啟新線程 2.串行執行任務 |
注意
使用sync函數往當前串行隊列中添加任務,會卡主當前的串行隊列。
GCD線程間的通信
- 有時候我們需要在子線程進行一些耗時操作,等耗時操作完成后再回到主線程進行相應的UI刷新,那么就可以使用下面的方式在子線程和主線程之間進行通信
- (void)test5
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"在%@線程中執行任務", [NSThread currentThread]);
// 回到主線程
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"在%@線程中執行任務", [NSThread currentThread]);
});
});
}
執行結果:
GCD延遲執行
- 特點:可以選擇在哪個線程中執行任務
- (void)test6
{
NSLog(@"方法開始運行");
/**
* GCD延遲執行方法
*
* 參數一: 要延遲的時間 (以秒為單位)
* 參數二: 在哪個線程中執行
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"GCD定時器");
});
}
執行結果:
一次性代碼
- 特點:
- 能保證整個程序運行過程中,block內的代碼塊只會被執行一次
- 線程是安全的
- 應用:
簡單的
單例模式(單例模式實現點我) - 注意點:不可放在懶加載中
- (void)test8
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"一次性代碼運行");
});
}
柵欄函數
- 作用:能夠控制并發隊列里面任務的執行順序
- 注意:不能使用全局并發隊列(會沒有任何區別,文檔中有注釋——只對自己創建的并發隊列有效)
- (void)test7
{
// 創建隊列
dispatch_queue_t queue = dispatch_queue_create(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"1");
}
});
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"2");
}
});
dispatch_barrier_async(queue, ^{
NSLog(@"進入柵欄函數");
});
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"3");
}
});
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"4");
}
});
}
執行結果:
GCD迭代開發(遍歷)
- 一般我們傳統的遍歷方式如下,它的缺點就是在處理比較耗時的操作時效率較低,因為只在一個線程中執行任務。
// 傳統的遍歷方式
for (int i ; i< 10; i++) {
NSLog(@"%d -- 當前線程%@", i, [NSThread currentThread]);
}
執行結果:
- 在GCD中,為我們提供了一個迭代函數,可以開啟子線程快速進行遍歷,這樣就可以大大提高效率,而且使用非常簡單。接下來使用迭代函數來進行文件復制的操作:
- (void)test9
{
// 獲得文件原始路徑(上層文件夾得路徑)
NSString *fromPath = @"/Users/yeshaojian/Desktop/test";
// 獲得文件的目標路徑
NSString *toPath = @"/Users/yeshaojian/Desktop/test2";
// 得到文件路徑下面的所有文件
NSArray *subpaths = [[NSFileManager defaultManager] subpathsAtPath:fromPath];
NSLog(@"文件名:%@",subpaths);
// 獲取數組中文件的個數
NSInteger count = subpaths.count;
// 將要迭代的操作放到迭代函數內
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t index){
// 拼接需要復制的文件的全路徑
NSString *fromFullpath = [fromPath stringByAppendingPathComponent:subpaths[index]];
// 拼接目標目錄的全路徑
NSString *toFullpath = [toPath stringByAppendingPathComponent:subpaths[index]];
// 執行文件剪切操作
/*
* 參數一:文件在哪里的全路徑
* 參數二:文件要被剪切到哪里的全路徑
*/
[[NSFileManager defaultManager] moveItemAtPath:fromFullpath toPath:toFullpath error:nil];
NSLog(@"拼接需要復制的文件的全路徑:%@ -- 拼接目標目錄的全路徑:%@ -- 當前線程:%@",fromFullpath,toFullpath,[NSThread currentThread]);
});
}
執行結果:
隊列組
- 假如開發中有多個任務,要求在所有任務都在子線程中并發執行,且不能使用柵欄函數,當所有任務都執行完成后打印“完成”。這樣的需求就需要用到GCD中的隊列組。
- 應用場合:
- 對多個任務有強制依賴性,缺一不可時使用
1.隊列組的基本使用
- (void)test10
{
// 獲取隊列組,用來管理隊列
dispatch_group_t group = dispatch_group_create();
// 獲取并發隊列
dispatch_queue_t queue = dispatch_queue_create("cs", DISPATCH_QUEUE_CONCURRENT);
// 添加任務
dispatch_group_async(group, queue, ^{
NSLog(@"cs1---%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"cs2---%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"cs3---%@", [NSThread currentThread]);
});
// 攔截通知:當隊列組中所有的任務都執行完畢后,會調用下面方法的block塊
dispatch_group_notify(group, queue, ^{
NSLog(@"完成");
});
}
執行結果:
隊列組函數內部操作簡要流程
處理流程:
1.封裝任務
2.把任務提交到隊列
3.把當前任務的執行情況納入到隊列注的監聽范圍
注意:下面方法本身是異步的
dispatch_group_notify(group, queue, ^{
});
拓展:
在一些框架或者早期項目中,可能會見到下面2種隊列組的使用方法,在這邊順帶提及一下,但不推薦使用,因為太過繁瑣。
第一種
- (void)test11
{
// 獲得隊列組,管理隊列
dispatch_group_t group = dispatch_group_create();
// 獲得并發隊列
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
// 表示開始把后面的異步任務納入到監聽范圍
//dispatch_group_enter & dispatch_group_leave
dispatch_group_enter(group);
// 使用異步函數封裝任務
dispatch_async(queue, ^{
NSLog(@"1---%@",[NSThread currentThread]);
// 通知隊列組該任務已經執行完畢
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"2---%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"3---%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
// 攔截通知
dispatch_group_notify(group, queue, ^{
NSLog(@"--完成---");
});
}
第二種
- (void)test11
{
// 獲得隊列組,管理隊列
dispatch_group_t group = dispatch_group_create();
// 獲得并發隊列
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
// 表示開始把后面的異步任務納入到監聽范圍
//dispatch_group_enter & dispatch_group_leave
dispatch_group_enter(group);
// 使用異步函數封裝任務
dispatch_async(queue, ^{
NSLog(@"1---%@",[NSThread currentThread]);
// 通知隊列組該任務已經執行完畢
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"2---%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"3---%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
// 等待DISPATCH_TIME_FOREVER 死等,一直要等到所有的任務都執行完畢之后才會繼續往下執行
// 同步執行
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 0.00001 * NSEC_PER_SEC);
// 等待timer m的時間 不管隊列中的任務有沒有執行完畢都繼續往下執行,如果在該時間內所有事任務都執行完畢了那么會返回一個0,否則是非0值
long n = dispatch_group_wait(group, timer);
NSLog(@"%ld",n);
NSLog(@"--完成---");
}
補充:同步\異步函數另一種創建方式
- 其實同步函數和異步函數還有另外的創建方式,但是使用起來比較不方便,所以上面就沒提及,想想還是補充一下好了
1.異步函數(創建一個使用函數封裝代碼的異步函數)
- (void)test12
{
/**
* 參數一:隊列
* 參數二:要傳給函數的參數
* 參數三:函數
*/
dispatch_async_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}
void testTask(void *param)
{
NSLog(@"%@", [NSThread currentThread]);
}
2.同步函數(創建一個使用函數封裝代碼的同步函數)
- (void)test12
{
/**
* 參數一:隊列
* 參數二:要傳給函數的參數
* 參數三:函數
*/
dispatch_sync_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}
void testTask(void *param)
{
NSLog(@"%@", [NSThread currentThread]);
}
上面使用的是函數來封裝要處理的代碼,使用比較不方便,且block是輕量級的數據結構,更推薦使用block封裝代碼的形式創建同步\異步函數。
GCD一些需要注意的細節
- 全局并發隊列是默認存在的(在我們程序運行的時候就存在)
- 全局隊列根據隊列的優先級分為 (高,默認,低,后臺優先級)4個并發隊列
- iOS 6之前,我們通過創建的線程,是要自己手動施放的
- 施放的方式 —— dispatch_release()
- 使用柵欄函數,蘋果官方文檔明確規定柵欄函數只有在和使用create函數創建的筆法隊列一起使用才有效
- 暫時就想到這么多O(∩_∩)O,因為GCD已經開源,想研究的朋友可以到網上搜索一下,有哪里不對的可以聯系我,謝謝!
NSOperation簡單使用
NSOperation作用
- 配合使用NSOperation和NSOperationQueue也能實現多線程編程
NSOperation和NSOperationQueue實現多線程的具體步驟
- 先將需要執行的操作封裝到一個NSOperation對象中
- 然后將NSOperation對象添加到NSOperation對象中
- 系統會自動將NSOperationQueue中的NSOperation取出來
- 將取出來的NSOperation封裝的操作放到一條新線程中執行
NSOperation的子類
- NSOperation是個抽象類,并不具備封裝操作的能力,必須使用它的子類
- 使用NSOperation子類的方式有3種
- NSInvocationOperation
- NSBlockOperation
- 自定義子類繼承NSOperation,實現內部相應的方法
NSOperation封裝操作
- 第一種方式 —— NSInvocationOperation
- (void)invocationTest
{
/**
* 參數一:目標對象
* 參數二:調用方法
*/
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
// 開啟任務
[op1 start];
}
- (void)download
{
NSLog(@"下載:%@",[NSThread currentThread]);
}
執行結果:需要和隊列并用才會開啟子線程執行任務
- 第二種方式 —— Block
- (void)blockTest
{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
[op1 addExecutionBlock:^{
NSLog(@"增加的下載:%@", [NSThread currentThread]);
}];
// 開啟任務
[op1 start];
[op2 start];
[op3 start];
}
- (void)download
{
NSLog(@"下載:%@",[NSThread currentThread]);
}
執行結果:如果一條線程中執行的操作大于1就會開啟新線程并發執行
- 方式三 —— 自定義NSOperation
1.先創建一個繼承自NSOperation的類并重寫main方法
- (void)main
{
NSLog(@"當前線程:%@", [NSThread currentThread]);
}
2.在需要使用的類中引用自定義的類,并創建開啟任務
- (void)custom
{
SJOperation *op1 = [[SJOperation alloc] init];
[op1 start];
}
執行結果:需要手動開啟線程或者與隊列并用才會開啟子線程
NSOperation中的隊列
- 主隊列 (獲取方式:+mainQueue)
- 所有在主隊列中的任務都在主線程中執行
- 本質上是串行隊列
- (void)invocationQueue
{
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
// 獲取主隊列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}
執行結果:所有任務都在主隊列中執行,且是串行隊列
- 非主隊列(獲取方式:alloc init)
- 同時具備并發和串行功能
- 默認下是并發的
- (void)invocationQueue
{
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
// 獲取非主隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}
執行結果:所有任務在子線程中并發執行
注意:addOperation:內部已經幫我們執行了開啟任務方法,所有不需要另外實現。
NSBlockOperation與隊列并用的簡單寫法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載:%@",[NSThread currentThread]);
}];
執行結果:所有任務都在子線程中并發執行
設置最大并發數
- 在NSOperation中,我們要想控制串行隊列或者并發隊列,只需要設置
maxConcurrentOperationCount
屬性即可- 一般我們要使用串行隊列,只需設置值為1即可
- 如果值大于1,則為并發隊列
1.串行隊列示例
- (void)blockQueue
{
// 創建非主隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 設置最大并發數為1,則隊列為串行隊列
queue.maxConcurrentOperationCount = 1;
[queue addOperationWithBlock:^{
NSLog(@"下載1:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載2:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載3:%@",[NSThread currentThread]);
}];
}
執行結果:按照任務添加順序執行,所以是串行隊列
2.并發隊列示例
- (void)blockQueue
{
// 創建非主隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 設置最大并發數為6,一般子線程控制在6以內,太多線程會使設備壓力過大
queue.maxConcurrentOperationCount = 6;
[queue addOperationWithBlock:^{
NSLog(@"下載1:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載2:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"下載3:%@",[NSThread currentThread]);
}];
}
執行結果:程序并沒有按照添加順序完成任務,所以是并發執行
注意:
- 一般子線程控制在6以內,太多線程會使設備壓力過大
-
maxConcurrentOperationCount
默認值為-1(在計算機中,-1一般指最大值) - 如果將
maxConcurrentOperationCount
設置為0,說明同一時間內執行0個任務,所以任務將不會執行。
NSOperation暫停、恢復和取消功能
- 在NSOperation中,已經為我們提供了暫停、恢復和取消的功能,我們只需調用相應的方法即可。
1.暫停
// 暫停
[queue setSuspended:YES];
2.恢復
// 取消
[queue setSuspended:NO];
3.取消
// 取消隊列中所有操作,且取消后的任務不可恢復
[queue cancelAllOperations];
注意:
1.隊列中的的任務是有狀態的,分別是 —— 等待;執行;完成三種狀態,且暫停、恢復和取消操作并不能作用于當前正處于執行狀態的任務,只能作用于等待狀態的任務。
2.如果是自定義的NSOperation,會發現暫停、恢復操作對其無效,對于這種情況,可以用以下方式解決 —— 使用取消操作
- (void)main
{
// 模擬耗時操作
for (int i = 0; i< 200; i++) {
NSLog(@"1當前線程:%@", [NSThread currentThread]);
}
// 判斷當前狀態,如果已經取消,直接返回
if (self.cancelled) return;
// 模擬耗時操作
for (int i = 0; i< 200; i++) {
NSLog(@"2當前線程:%@", [NSThread currentThread]);
}
// 判斷當前狀態,如果已經取消,直接返回
if (self.cancelled) return;
// 模擬耗時操作
for (int i = 0; i< 200; i++) {
NSLog(@"3當前線程:%@", [NSThread currentThread]);
}
// 判斷當前狀態,如果已經取消,直接返回
if (self.cancelled) return;
}
解決問題思路:其實這是蘋果官方文檔中的建議 —— 因為,當我們調用cancelAllOperations:
方法的時候,他內部的cancelled
屬性就會為真,每執行完一個耗時操作后都進行一次判斷,如果發現已經取消,則退出執行
。如果想更精確操控的話,也可以將判斷操作放到耗時操作中,但是不建議這樣做,因為這樣性能極差。
NSOperation中的依賴操作
- NSOperation提供了一套非常便捷好用的操作依賴方式,比起GCD,那種酸爽簡直不敢相信
- (void)blockQueue
{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載1:%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載2:%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載3:%@",[NSThread currentThread]);
}];
// 設置依賴關系
// op1依賴op2,只有當op2執行完畢后,才會執行op1
[op1 addDependency:op2];
// op2依賴op3,只有當op3執行完畢后,才會執行op2
[op2 addDependency:op3];
// 獲取主隊列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
執行結果:先執行完op3,等op3執行完成后才執行op2,當op2執行完畢后,才執行op1
注意
- NSOperation提供的操作依賴功能特別強大,可以設置不同隊列的依賴
- 但是不能循環依賴,比如op1依賴op2,op2又依賴op1,而且并不會報錯,但會發生死鎖,且有關任務都不執行。
NSOperation的監聽
- 我們經常有這樣的需要:在某些任務執行完成后,再執行指定的某些操作,那么NSOperation中的監聽功能就派上用場了,使用非常簡單
NSOperation *op = [[NSOperation alloc] init];
op.completionBlock = ^{
NSLog(@"下載完成");
};
[op start];
NSOperation線程間通信
- NSOperation線程間的通信類似于GCD,所以就不多敘述了,直接上代碼
- (void)downloadPhoto
{
// 獲取非主隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 創建下載任務
[queue addOperationWithBlock:^{
// 圖片地址
NSURL *url = [NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201512/05/20151205092106_aksZU.jpeg"];
// 下載圖片
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 轉換圖片
UIImage *image = [UIImage imageWithData:imageData];
// 回到主線程刷新
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回%@線程刷新UI", [NSThread currentThread]);
self.imageView.image = image;
}];
}];
}
執行結果:
GCD和NSOperation區別
在開發中最常用的就是GCD和NSOperation來進行多線程開發,NSThread更多是在測試時輔助使用,pthread則很少看見,這里為大家簡單整理一下他們之間的區別
-
GCD和NSOperation的對比
- GCD是純C語言的API,而操作隊列(NSOperation)則是Object-C的對象
- 在GCD中,任務用Block塊來表示,而塊是輕量級的數據結構,相反,操作隊列(NSOperation)中的
操作
NSOperation是比較重量級的Object-C對象
-
那么在開發中如何選擇呢?
- 一般如果任務之間有依賴關系或者需要監聽任務執行的過程(KVO),首選NSOperation
- 單純進行一些耗時操作則選用GCD,因為相比NSOperation,GCD效率更高,性能更好
-
NSOperation和NSOperationQueue好處
- NSOperation可以方便設置操作優先級(表示操作在隊列中與其它操作之間的優先關系,級別越高越先執行)
- NSOperation可以通過KVO的方式對NSOperation對象進行監聽控制(監聽當前操作是處于完成,取消還是執行狀態)
- NSOperation可以方便設置操作之間的依賴關系
- 通過自定義NSOperation子類可以實現操作復用