進程與線程
進程:計算機操作系統(tǒng)分配資源的單位塑径,是指系統(tǒng)中正在運行的應用程序衔彻,進程之間相互獨立,運行在受保護的內存空間幅疼,比如同時打開XCode米奸、QQ,系統(tǒng)就會啟動兩個進程爽篷;
線程:進程的基本執(zhí)行單元悴晰,一個進程中的任務都在線程中執(zhí)行;
并發(fā)與并行
并發(fā):并發(fā)的關鍵是具有處理多個任務的能力逐工,不一定要同時铡溪;
并行:并行的關鍵是你有同時處理多個任務的能力。
你吃飯吃到一半泪喊,電話來了棕硫,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行袒啼。
你吃飯吃到一半哈扮,電話來了,你一手筷子蚓再,一手電話滑肉,說一句話,咽一口飯摘仅。這說明你支持并發(fā)靶庙。
你吃飯吃到一半,電話來了娃属,你一邊打電話一邊吃飯六荒,這就需要兩張嘴,也就是多核CPU矾端,這說明你支持并行掏击。
同步與異步
同步方法就是我們平時調用的哪些方法。因為任何有編程經(jīng)驗的人都知道秩铆,比如在第一行調用foo()方法铐料,那么程序運行到第二行的時候,foo方法肯定是執(zhí)行完了豺旬。
所謂的異步钠惩,就是允許在執(zhí)行某一個任務時,函數(shù)立刻返回族阅,但是真正要執(zhí)行的任務稍后完成篓跛。比如我們在點擊保存按鈕之后,要先把數(shù)據(jù)寫到磁盤坦刀,然后更新UI愧沟。同步方法就是等到數(shù)據(jù)保存完再更新UI蔬咬,而異步則是立刻從保存數(shù)據(jù)的方法返回并向后執(zhí)行代碼,同時真正用來保存數(shù)據(jù)的指令將在稍后執(zhí)行沐寺。
多線程優(yōu)缺點
- 優(yōu)點:能適當提高程序的執(zhí)行效率林艘,能適當提高資源利用率(CPU、內存利用率)
- 缺點:創(chuàng)建線程是有開銷的混坞,iOS下主要成本包括:內核數(shù)據(jù)結構(大約1KB)狐援、棧空間(子線程512KB究孕、主線程1MB啥酱,也可以使用-setStackSize:設置,但必須是4K的倍數(shù)厨诸,而且最小是16K)镶殷,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時間
如果開啟大量的線程,會降低程序的性能微酬,線程越多绘趋,CPU在調度線程上的開銷就越大。
程序設計更加復雜:比如線程之間的通信颗管、多線程的數(shù)據(jù)共享等問題埋心。
iOS中多線程解決方案
1. pthread
pthread 是一套通用的多線程的 API,可以在Unix / Linux / Windows 等系統(tǒng)跨平臺使用忙上,使用 C 語言編寫,需要程序員自己管理線程的生命周期闲坎,使用較為復雜疫粥,我們在 iOS 開發(fā)中幾乎不使用 pthread,我們可以稍作了解腰懂。
2. NSThread
NSThread 是蘋果官方提供的梗逮,使用起來比 pthread 更加面向對象,簡單易用绣溜,可以直接操作線程對象慷彤。不過也需要需要程序員自己管理線程的生命周期(主要是創(chuàng)建),我們在開發(fā)的過程中偶爾使用 NSThread怖喻。比如我們會經(jīng)常調用[NSThread currentThread]來顯示當前的進程信息底哗。
創(chuàng)建方式
-
先創(chuàng)建再啟動
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"NSThread1"]; [thread1 start];
-
創(chuàng)建線程后自動啟動
[NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"NSThread2"];
-
隱式創(chuàng)建線程,直接啟動
[self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];
相關方法
-
類方法
// 當前線程 [NSThread currentThread]; // 打印結果:{number = 1, name = main} NSLog(@"%@",[NSThread currentThread]); //休眠多久 [NSThread sleepForTimeInterval:2]; //休眠到指定時間 [NSThread sleepUntilDate:[NSDate date]]; //退出線程 [NSThread exit]; //判斷當前線程是否為主線程 [NSThread isMainThread]; //判斷當前線程是否是多線程 [NSThread isMultiThreaded]; //主線程的對象 NSThread *mainThread = [NSThread mainThread];
-
實例方法
//線程是否在執(zhí)行 thread.isExecuting; //線程是否被取消 thread.isCancelled; //線程是否完成 thread.isFinished; //是否是主線程 thread.isMainThread; //線程的優(yōu)先級,取值范圍0.0到1.0锚沸,默認優(yōu)先級0.5跋选,1.0表示最高優(yōu)先級,優(yōu)先級高哗蜈,CPU調度的頻率高 thread.threadPriority;
線程間通信
在開發(fā)中前标,線程往往不是孤立存在的坠韩,多個線程之間需要經(jīng)常進行通信我們經(jīng)常會在子線程進行耗時操作,操作結束后再回到主線程去刷新 UI炼列。這就涉及到了子線程和主線程之間的通信只搁。我們先來了解一下官方關于 NSThread 的線程間通信的方法。
// 在主線程上執(zhí)行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes
// 在指定線程上執(zhí)行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// 在當前線程上執(zhí)行操作俭尖,調用 NSObject 的 performSelector:相關方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
下面通過一個經(jīng)典的下載圖片 DEMO 來展示線程之間的通信氢惋。具體步驟如下:
1.開啟一個子線程亏狰,在子線程中下載圖片跌帐。
2.回到主線程刷新 UI,將圖片展示在 UIImageView 中簸州。
代碼如下:
/**
* 創(chuàng)建一個線程下載圖片
*/
- (void)downloadImageOnSubThread {
// 在創(chuàng)建的子線程中調用downloadImage下載圖片
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
/**
* 下載圖片缭付,下載完之后回到主線程進行 UI 刷新
*/
- (void)downloadImage {
NSLog(@"current thread -- %@", [NSThread currentThread]);
// 1. 獲取圖片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
// 2. 從 imageUrl 中讀取數(shù)據(jù)(下載圖片) -- 耗時操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通過二進制 data 創(chuàng)建 image
UIImage *image = [UIImage imageWithData:imageData];
// 3. 回到主線程進行圖片賦值和界面刷新
[self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}
/**
* 回到主線程進行圖片賦值和界面刷新
*/
- (void)refreshOnMainThread:(UIImage *)image {
NSLog(@"current thread -- %@", [NSThread currentThread]);
// 賦值圖片到imageview
self.imageView.image = image;
}
線程安全
多線程安全隱患的原因:一塊資源可能會被多個線程共享柿估,也就是多個線程可能會訪問同一塊資源。當多個線程訪問同一塊資源時陷猫,很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題秫舌。就好比幾個人在同一時修改同一個表格,造成數(shù)據(jù)的錯亂绣檬。
解決方法:
-
添加互斥鎖:
@synchronized(鎖對象) { // 需要鎖定的代碼 }
iOS 實現(xiàn)線程加鎖有很多種方式足陨。@synchronized、 NSLock娇未、NSRecursiveLock墨缘、NSCondition、NSConditionLock等等各種方式零抬。判斷的時候鎖對象要存在镊讼,如果代碼中只有一個地方需要加鎖,大多都使用self作為鎖對象平夜,這樣可以避免單獨再創(chuàng)建一個鎖對象蝶棋。加了互斥做的代碼,當新線程訪問時忽妒,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼玩裙,新線程就會進入休眠。
- 自旋鎖:
加了自旋鎖段直,當新線程訪問代碼時吃溅,如果發(fā)現(xiàn)有其他線程正在鎖定代碼,新線程會用死循環(huán)的方式鸯檬,一直等待鎖定的代碼執(zhí)行完成罕偎。相當于不停嘗試執(zhí)行代碼,比較消耗性能京闰。
屬性修飾atomic本身就有一把自旋鎖:
nonatomic 非原子屬性,同一時間可以有很多線程讀和寫
atomic 原子屬性(線程安全)颜及,保證同一時間只有一個線程能夠寫入(但是同一個時間多個線程都可以取值)甩苛,atomic 本身就有一把鎖(自旋鎖)
atomic:線程安全,需要消耗大量的資源
nonatomic:非線程安全俏站,不過效率更高讯蒲,一般使用nonatomic
下面通過一個售票實例來看一下鎖的作用:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread01;
@property(nonatomic,strong)NSThread *thread02;
@property(nonatomic,strong)NSThread *thread03;
@property(nonatomic,assign)NSInteger numTicket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 總票數(shù)為30
self.numTicket = 30;
self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread01.name = @"售票員01";
self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread02.name = @"售票員02";
self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread03.name = @"售票員03";
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread01 start];
[self.thread02 start];
[self.thread03 start];
}
// 售票
-(void)saleTicket
{
while (1) {
// 鎖對象,本身就是一個對象肄扎,所以self就可以了
// 鎖定的時候墨林,其他線程沒有辦法訪問這段代碼
@synchronized (self) {
// 模擬售票時間,我們讓線程休息0.05s
[NSThread sleepForTimeInterval:0.05];
if (self.numTicket > 0) {
self.numTicket -= 1;
NSLog(@"%@賣出了一張票犯祠,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
}else{
NSLog(@"票已經(jīng)賣完了");
break;
}
}
}
}
@end
我們可以看到?jīng)]有加鎖時有的票被多賣了旭等,顯然不對,接下來看看加鎖的結果:
加上互斥鎖后衡载,就不會出現(xiàn)數(shù)據(jù)錯亂的情況了搔耕。
GCD
GCD是蘋果公司為多核的并行運算提出的解決方案,它可以自動管理線程的生命周期(創(chuàng)建線程痰娱、調度任務弃榨、銷毀線程),我們只需要告訴GCD想要執(zhí)行什么任務梨睁,不需要編寫任何線程管理代碼鲸睛。
GCD中的任務與隊列
任務:GCD以block為基本單位,一個block中的代碼可以為一個任務坡贺。
任務有兩種執(zhí)行方式: 同步函數(shù) 和 異步函數(shù)官辈,他們之間的區(qū)別是:
- 同步:只能在當前線程中執(zhí)行任務,不具備開啟新線程的能力遍坟,任務立刻馬上執(zhí)行拳亿,會阻塞當前線程并等待 Block中的任務執(zhí)行完畢,然后當前線程才會繼續(xù)往下運行
- 異步:可以在新的線程中執(zhí)行任務政鼠,具備開啟新線程的能力,但不一定會開新線程队魏,當前線程會直接往下執(zhí)行公般,不會阻塞當前線程
隊列:裝載線程任務的隊形結構。(系統(tǒng)以先進先出的方式調度隊列中的任務執(zhí)行)胡桨。在GCD中有兩種隊列:串行隊列和并發(fā)隊列官帘。
- 串行隊列(Serial Dispatch Queue):讓任務一個接著一個地執(zhí)行(一個任務執(zhí)行完畢后,再執(zhí)行下一個任務)
- 并發(fā)隊列(Concurrent Dispatch Queue):可以讓多個任務并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務)昧谊,并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效刽虹。
GCD的使用分為兩步:
- 添加任務;
- 將任務放到指定的隊列中呢诬,GCD自動將任務取出放到對應的線程中執(zhí)行涌哲。
GCD的創(chuàng)建
- 創(chuàng)建隊列
使用dispatch_queue_create來創(chuàng)建隊列對象胖缤,傳入兩個參數(shù),第一個參數(shù)表示隊列的唯一標識符阀圾,可為空哪廓。第二個參數(shù)用來表示隊列的類型,串行隊列(DISPATCH_QUEUE_SERIAL)或并發(fā)隊列(DISPATCH_QUEUE_CONCURRENT)初烘。
串行隊列:
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
并發(fā)隊列:
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);
全局并發(fā)隊列:GCD默認已經(jīng)提供了全局并發(fā)隊列涡真,供整個應用使用,可以無需手動創(chuàng)建:
/**
第一個參數(shù):優(yōu)先級 也可直接填后面的數(shù)字
#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 // 后臺
第二個參數(shù): 預留參數(shù) 0
*/
dispatch_queue_t quque1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主隊列:GCD 提供了的一種特殊的串行隊列肾筐,主隊列負責在主線程上調度任務哆料,如果在主線程上已經(jīng)有任務正在執(zhí)行,主隊列會等到主線程空閑后再調度任務吗铐。通常是返回主線程更新UI的時候使用东亦。dispatch_get_main_queue():
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 耗時操作放在這里
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主線程進行UI操作
});
});
- 執(zhí)行任務
同步(Synchronize)使用dispatch_sync;
/*
第一個參數(shù):隊列
第二個參數(shù):block,在里面封裝任務
*/
dispatch_sync(queue, ^{
});
異步(asynchronous)使用dispatch_async抓歼;
dispatch_async(queue, ^{
});
GCD的使用:隊列和任務的組合
當在主隊列中加入同步函數(shù)的時候讥此,會造成死鎖。
//1.獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
//2.同步函數(shù)
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
主隊列在執(zhí)行dispatch_sync谣妻,隨后隊列中新增一個任務block萄喳。因為主隊列是同步隊列,所以block要等dispatch_sync執(zhí)行完才能執(zhí)行蹋半,但是dispatch_sync是同步派發(fā)他巨,要等block執(zhí)行完才算是結束。在主隊列中的兩個任務互相等待减江,導致了死鎖染突。
解決方案:其實在通常情況下我們不必要用dispatch_sync,因為dispatch_async能夠更好的利用CPU辈灼,提升程序運行速度份企。只有當我們需要保證隊列中的任務必須順序執(zhí)行時,才考慮使用dispatch_sync巡莹。在使用dispatch_sync的時候應該分析當前處于哪個隊列司志,以及任務會提交到哪個隊列。
注意:GCD中開多少條線程是由系統(tǒng)根據(jù)CUP繁忙程度決定的降宅,如果任務很多骂远,GCD會開啟適當?shù)淖泳€程,并不會讓所有任務同時執(zhí)行腰根。
- GCD線程間的通信非常簡單激才,使用同步或異步函數(shù),傳入主隊列即可(就像上面介紹主隊列時那樣):
-(void)downloadImage
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
// 獲得圖片URL
NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
// 將圖片URL下載為二進制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 將二進制文件轉化為image
UIImage *image = [UIImage imageWithData:data];
NSLog(@"%@",[NSThread currentThread]);
// 返回主線程 這里用同步函數(shù)不會發(fā)生死鎖,因為這個方法在子線程中被調用瘸恼。
// 也可以使用異步函數(shù)
dispatch_sync(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@",[NSThread currentThread]);
});
});
}
GCD其他常用方法
1. 柵欄函數(shù)(控制任務的執(zhí)行順序)
當任務需要異步進行劣挫,但是這些任務需要分成兩組來執(zhí)行,第一組完成之后才能進行第二組的操作钞脂。這時候就用了到GCD的柵欄方法dispatch_barrier_async:
-(void)barrier
{
//1.創(chuàng)建隊列(并發(fā)隊列)
dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
}
});
//柵欄函數(shù)
dispatch_barrier_async(queue, ^{
NSLog(@"這是一個柵欄函數(shù)揣云,34任務在12之后進行");
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
}
});
}
2. 延遲執(zhí)行
/*
第一個參數(shù):延遲時間
第二個參數(shù):要執(zhí)行的代碼
如果想讓延遲的代碼在子線程中執(zhí)行,也可以更改在哪個隊列中執(zhí)行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"---%@",[NSThread currentThread]);
});
當然冰啃,除了GCD以外我們還有其他的方法:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
3.使代碼只執(zhí)行一次
//整個程序運行過程中只會執(zhí)行一次
//onceToken用來記錄該部分的代碼是否被執(zhí)行過
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----");
});
這個用法一般用在單例模式中邓夕。
4.dispatch_apply(快速迭代)
dispatch_apply函數(shù)是dispatch_sync函數(shù)和Dispatch Group的關聯(lián)API,該函數(shù)按指定的次數(shù)將指定的Block追加到指定的Dispatch Queue中,并等到全部的處理執(zhí)行結束:
- (void)dispatchApplyTest1 {
//生成全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/**
* @param 10 指定重復次數(shù) 指定10次
* @param queue 追加對象的Dispatch Queue
* @param index 帶有參數(shù)的Block, index的作用是為了按執(zhí)行的順序區(qū)分各個Block
*
*/
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu-----%@", index, [NSThread currentThread]);
});
NSLog(@"finished");
}
可以看到該函數(shù)開啟了多個線程執(zhí)行block里的操作,我們可以利用這個特性模擬循環(huán)完成快速迭代遍歷(無序):
- (void)dispatchApplyTest2 {
//1.創(chuàng)建NSArray類對象
NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];
//2.創(chuàng)建一個全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.通過dispatch_apply函數(shù)對NSArray中的全部元素進行處理,并等待處理完成,
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});
NSLog(@"finished");
}
隊列組
異步執(zhí)行幾個耗時操作阎毅,當這幾個操作都完成之后再回到主線程進行操作焚刚,這是就要用到隊列組了,隊列組可以用來管理隊列中任務的執(zhí)行扇调。
// 創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
// 創(chuàng)建并行隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 執(zhí)行隊列組任務
dispatch_group_async(group, queue, ^{
});
//隊列組中的任務執(zhí)行完畢之后矿咕,執(zhí)行該函數(shù)
dispatch_group_notify(group, queue, ^{
});
將兩張圖片分別下載完成后,合成一張圖片并顯示的例子:
-(void)GCDGroup
{
//下載圖片1
//創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
//1.開子線程下載圖片
//創(chuàng)建隊列(并發(fā))
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
//1.獲取url地址
NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-61b8a20c108f539d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
//2.下載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二進制數(shù)據(jù)轉換成圖片
self.image1 = [UIImage imageWithData:data];
NSLog(@"1---%@",self.image1);
});
//下載圖片2
dispatch_group_async(group, queue, ^{
//1.獲取url地址
NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-2a0505c7992fd970.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
//2.下載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二進制數(shù)據(jù)轉換成圖片
self.image2 = [UIImage imageWithData:data];
NSLog(@"2---%@",self.image2);
});
//合成狼钮,隊列組執(zhí)行完畢之后執(zhí)行
dispatch_group_notify(group, queue, ^{
//開啟圖形上下文
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
//畫1
[self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
//畫2
[self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
//根據(jù)圖形上下文拿到圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
//關閉上下文
UIGraphicsEndImageContext();
//回到主線程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@--刷新UI",[NSThread currentThread]);
});
});
}
GCD信號量(dispatch_semaphore)
信號量:就是一種可用來控制訪問資源的數(shù)量的標識碳柱,設定了一個信號量,在線程訪問之前熬芜,加上信號量的處理莲镣,則可告知系統(tǒng)按照我們指定的信號量數(shù)量來執(zhí)行多個線程。其實涎拉,這有點類似鎖機制了瑞侮,只不過信號量都是系統(tǒng)幫助我們處理了,我們只需要在執(zhí)行線程之前鼓拧,設定一個信號量值半火,并且在使用時,加上信號量處理方法就行了季俩。主要有三個方法:
//創(chuàng)建信號量钮糖,參數(shù):信號量的初值,如果小于0則會返回NULL
dispatch_semaphore_create(long value)
//等待,降低信號量
dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)
//提高信號量,這個函數(shù)會使傳入的信號量dsema的值加1
dispatch_semaphore_signal(dispatch_semaphore_t semaphore)
關于信號量酌住,可以用停車來比喻店归。
停車場剩余4個車位,那么即使同時來了四輛車也能停的下赂韵。如果此時來了五輛車娱节,那么就有一輛需要等待挠蛉。
信號量的值就相當于剩余車位的數(shù)目祭示,dispatch_semaphore_wait函數(shù)就相當于來了一輛車,dispatch_semaphore_signal
就相當于走了一輛車。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value))质涛,調用一次dispatch_semaphore_signal稠歉,剩余的車位就增加一個;調用一次dispatch_semaphore_wait剩余車位就減少一個汇陆;當剩余車位為0時怒炸,再來車(即調用dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位毡代。有些車主沒有耐心阅羹,給自己設定了一段等待時間,這段時間內等不到停車位就走了教寂,如果等到了就開進去停車捏鱼。而有些車主就像把車停在這,所以就一直等下去酪耕。
我們看個例子导梆,假設現(xiàn)在系統(tǒng)有兩個空閑資源可以被利用,但同一時間卻有三個線程要進行訪問迂烁,這時利用GCD信號量代碼如下:
-(void)dispatchSignal{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任務1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});
//任務2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任務3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
}
我們可以看到任務1和任務3首先搶到了這兩塊資源看尼,有任務完成后才輪到任務二。
接下來我們還是以售賣車票為例盟步,用信號量怎么實現(xiàn)加鎖功能:
dispatch_semaphore_t semaphore;
- (void)viewDidLoad {
[super viewDidLoad];
// 總票數(shù)為30
self.numTicket = 35;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票員1";
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票員2";
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票員3";
[self saleTicket];
});
semaphore = dispatch_semaphore_create(1);
}
// 售票
-(void)saleTicket
{
while (1) {
[NSThread sleepForTimeInterval:0.05];
if (self.numTicket > 0) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
self.numTicket -= 1;
NSLog(@"%@賣出了一張票藏斩,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
}else{
NSLog(@"票已經(jīng)賣完了");
break;
}
dispatch_semaphore_signal(semaphore);
}
}
信號量屬于底層工具。它非常強大址芯,但在多數(shù)需要使用它的場合灾茁,最好從設計角度重新考慮,看是否可以不用谷炸。應該優(yōu)先考慮是否可以使用諸如操作隊列這樣的高級工具北专。通常可以通過增加一個分派隊列dispatch_suspend旬陡,或者通過其他方式分解操作來避免使用信號量拓颓。信號量并非不好,只是它本身是鎖描孟,能不用鎖就不要用驶睦。盡量用cocoa框架中的高級抽象,信號量非常接近底層匿醒。但有時候场航,例如需要把異步任務轉換為同步任務時,信號量是最合適的工具廉羔。
NSOperation
NSOperation 是蘋果公司對 GCD 的封裝溉痢,完全面向對象,并比GCD多了一些更簡單實用的功能。NSOperation需要配合NSOperationQueue來實現(xiàn)多線程孩饼。NSOperation 和NSOperationQueue 分別對應 GCD 的 任務 和 隊列髓削。
使用步驟:
- 將需要執(zhí)行的操作封裝到一個NSOperation對象中;
- 將NSOperation對象添加到NSOperationQueue中镀娶,系統(tǒng)會自動將NSOperationQueue中的NSOperation取出來立膛,并將取出的NSOperation封裝的操作放到一條新線程中執(zhí)行。
NSOperation是個抽象類梯码,實際運用時中需要使用它的子類宝泵,有三種方式:
- 使用子類NSInvocationOperation
- 使用子類NSBlockOperation
- 定義繼承自NSOperation的子類,通過實現(xiàn)內部相應的方法來封裝任務轩娶。
NSOperation 的創(chuàng)建
- NSInvocationOperation
/*
第一個參數(shù):目標對象
第二個參數(shù):選擇器,要調用的方法
第三個參數(shù):方法要傳遞的參數(shù)
*/
NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//啟動操作
[op start];
- NSBlockOperation
//1.封裝操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//要執(zhí)行的操作鲁猩,在主線程中執(zhí)行
NSLog(@"1------%@",[NSThread currentThread]);
}];
//2.追加操作,追加的操作在子線程中執(zhí)行罢坝,可以追加多條操作
[op addExecutionBlock:^{
NSLog(@"---download2--%@",[NSThread currentThread]);
}];
[op start];
NSBlockOperation 還提供了一個方法 addExecutionBlock:廓握,通過 addExecutionBlock: 就可以為 NSBlockOperation 添加額外的操作。這些操作(包括 blockOperationWithBlock 中的操作)可以在不同的線程中同時(并發(fā))執(zhí)行嘁酿。只有當所有相關的操作已經(jīng)完成執(zhí)行時隙券,才視為完成。如果添加的操作多的話闹司,blockOperationWithBlock: 中的操作也可能會在其他線程(非當前線程)中執(zhí)行娱仔,這是由系統(tǒng)決定的,并不是說添加到 blockOperationWithBlock: 中的操作一定會在當前線程中執(zhí)行游桩。
- 自定義繼承自 NSOperation 的子類
如果使用子類 NSInvocationOperation牲迫、NSBlockOperation 不能滿足日常需求,我們可以使用自定義繼承自 NSOperation 的子類借卧№镌鳎可以通過重寫 main 或者 start 方法 來定義自己的 NSOperation 對象。重寫main方法比較簡單铐刘,我們不需要管理操作的狀態(tài)屬性 isExecuting 和 isFinished陪每。當 main 執(zhí)行完返回的時候,這個操作就結束了镰吵。
定義:
// YSCOperation.h 文件
#import <Foundation/Foundation.h>
@interface JYHOperation : NSOperation
@end
//JYHOperation.m 文件
#import "JYHOperation.h"
@implementation JYHOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:3];
NSLog(@"%@", [NSThread currentThread]);
}
}
}
@end
使用
- (void)useCustomOperation {
// 1.創(chuàng)建 Operation 對象
JYHOperation *op = [[JYHOperation alloc] init];
// 2.調用 start 方法開始執(zhí)行
[op start];
}
在沒有使用 NSOperationQueue檩禾、在主線程單獨使用自定義繼承自 NSOperation 的子類以及使用NSInvocationOperation的情況下疤祭,是在主線程執(zhí)行操作,并沒有開啟新線程戏售,接下來看看怎么將操作添加到隊列中去啦辐。
創(chuàng)建NSOperationQueue
一共有兩種隊列:
- 主隊列:通過mainQueue獲得,凡是放到主隊列中的任務都將在主線程執(zhí)行;
- 非主隊列:通過 alloc init創(chuàng)建续挟,非主隊列同時具備了并發(fā)和串行的功能,通過設置最大并發(fā)數(shù)屬性來控制任務是并發(fā)執(zhí)行還是串行執(zhí)行
將操作添加到隊列的方式也有兩種:
- 先創(chuàng)建操作诗祸,再將創(chuàng)建好的操作加入到創(chuàng)建好的隊列中去:
-(void)addOperation:(NSOperation *)op;
- 無需先創(chuàng)建操作跑芳,在 block 中添加操作,直接將包含操作的 block 加入到隊列中:
- (void)addOperationWithBlock:(void (^)(void))block;
將操作加入到操作隊列后能夠開啟新線程博个,并發(fā)執(zhí)行功偿。并且將操作添加到NSOperationQueue中械荷,就會自動啟動,不需要再自己啟動了吨瞎。
NSOperationQueue控制串行颤诀、并行
NSOperationQueue有個關鍵屬性 maxConcurrentOperationCount,叫做最大并發(fā)操作數(shù),用來控制一個特定隊列中可以有多少個操作同時參與并發(fā)執(zhí)行遗淳。
- maxConcurrentOperationCount默認為-1洲脂,直接并發(fā)執(zhí)行剧包,所以加入到‘非隊列’中的任務默認就是并發(fā)疆液,開啟多線程。
- 當maxConcurrentOperationCount為1時潘飘,則表示不開線程卜录,也就是串行。
- 當maxConcurrentOperationCount大于1時筐高,進行并發(fā)執(zhí)行。
- 系統(tǒng)對最大并發(fā)數(shù)有一個限制柑土,所以即使把maxConcurrentOperationCount設置的很大稽屏,系統(tǒng)也會自動調整西乖。所以把最大并發(fā)數(shù)設置的很大是沒有意義的获雕。
- maxConcurrentOperationCount 控制的不是并發(fā)線程的數(shù)量,而是一個隊列中同時能并發(fā)執(zhí)行的最大操作數(shù)被廓。
NSOperation 操作依賴
NSOperation能添加操作之間的依賴關系萝玷。通過操作依賴球碉,我們可以很方便的控制操作之間的執(zhí)行先后順序。
NSOperation 提供管理依賴的接口:
- 添加依賴:
- (void)addDependency:(NSOperation *)op;
- 移除依賴:
- (void)removeDependency:(NSOperation *)op;
比如說有 A挎春、B 兩個操作豆拨,其中 A 執(zhí)行完操作施禾,B 才能執(zhí)行操作。
如果使用依賴來處理的話邮绿,那么就需要讓操作 B 依賴于操作 A:
- (void)addDependency {
// 1.創(chuàng)建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.創(chuàng)建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
}
}];
// 3.添加依賴
[op2 addDependency:op1]; // 讓op2 依賴于 op1,則先執(zhí)行op1顾腊,在執(zhí)行op2
// 4.添加操作到隊列中
[queue addOperation:op1];
[queue addOperation:op2];
}
NSOperation杂靶、NSOperationQueue 常用屬性和方法
- NSOpreation
// 開啟線程
- (void)start;
- (void)main;
// 判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消當前線程
- (void)cancel;
//NSOperation任務是否在運行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任務是否已結束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依賴
- (void)addDependency:(NSOperation *)op;
// 移除依賴
- (void)removeDependency:(NSOperation *)op;
// 優(yōu)先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
// 操作監(jiān)聽
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞當前線程冠骄,直到該NSOperation結束凛辣≈吧眨可用于線程執(zhí)行順序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 獲取線程的優(yōu)先級
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
- NSOperationQueue
// 獲取隊列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 隊列中的操作數(shù)
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并發(fā)數(shù)蚀之,同一時間最多只能執(zhí)行三個操作
@property NSInteger maxConcurrentOperationCount;
// 暫停 YES:暫停 NO:繼續(xù)
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞當前線程直到此隊列中的所有任務執(zhí)行完畢
- (void)waitUntilAllOperationsAreFinished;