多線程
什么是多線程?
多線程就是一個進程中可以開啟多條線程,每條線程可以并行執行不同的任務,提高執行效率;一個基本概念就是同時對多個任務加以控制;
多線程分為主線程和子線程;一個iOS程序運行后,默認會開啟1條線程,稱為“主線程”或“UI線程”;主線程的主要作用顯示\刷新UI界面\處理UI事件(比如點擊事件、滾動事件、拖拽事件等);子線程主要用來做一些耗時操作;
每一個iOS應用程序中都有一個主線程用來更新UI界面、處理用戶的觸摸事件、解析網絡下載的數據,因此不能把一些太耗時的操作(比如網絡下載數據)放在主線程中執行,不然會造成主線程堵塞(出現界面卡死,防止界面假死),帶來極壞的用戶體驗。
iOS的解決方案就是將那些耗時的操作放到另外一個線程中去執行,多線程異步執行是防止主線程堵塞,增加運行效率的最佳方法;
多線程的工作原理:
- 同一時間,CPU只能處理1條線程,只有1條線程在工作(執行)
- 多線程并發(同時)執行,其實是CPU快速地在多條線程之間調度 (切換)
- 如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象
多線程的優缺點:
優點:
- 能適當提高程序的執行效率
- 能適當提高資源利用率(CPU、內存利用率)
缺點:
- 開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能
- 線程越多,CPU在調度線程上的開銷就越大
- 程序設計更加復雜:比如線程之間的通信、多線程的數據共享
- 數據庫訪問的安全問題
多線程的使用場合:
- 異步下載數據,是多線程技術的一個比較常見的應用場景
- APP中有耗時的操作或功能(1、從數據庫中一次性讀取大量數據 2、對大量數據的解析過程),需要在主線程之外,單獨開辟一個新的線程(子線程/工作線程)來執行
多線程創建方式:
pthread
NSThread
GCD
NSOperation & NSOperationQueue
創建方式 | 使用簡介 | 所用語言 | 生命周期管理 | 使用頻率 |
---|---|---|---|---|
pthread | 一套通用的多線程API;跨平臺\可移植;使用難度大 | C | 程序員管理 | 幾乎不用 |
NSThread | 使用更加面向對象;可直接操作現場對象 | OC | 程序員管理 | 偶爾使用 |
GCD | 充分利用設備的多核問題 | C | 自動管理 | 經常使用 |
NSOperation | 基于 GCD(底層是 GCD) ;比 GCD 多一些方法和功能 | OC | 自動管理 | 經常使用 |
pthread
這是一套在很多操作系統上都通用的多線程API,所以移植性很強,當然在 iOS 中也是可以的。不過這是基于 c語言 的框架,同時也適用于Unix\Linux\Windows等系統;使用難度比較大;
使用流程:
首先導入頭文件:
#######import <pthread.h>
創建線程,執行耗時操作
//1.創建一個pThread 變量(就是一個線程)
pthread_t pthread;
//2.通過pthread_create函數創建一個pthread_t,給pthread變量賦值
/**
參數:
1.傳入一個pthread_t類型的變量的地址
2.線程屬性,默認給NULL
3.傳入一個C語言的函數名,其實就是需要執行的耗時操作
4.給func傳參
*/
//__bridge橋聯(橋接)
//在ARC中將id 臨時轉換成void*。(在MRC中在這兒不需要__bridge來進行轉換)
//str的所有權屬不變,只是臨時被借出去用了一下
//ARC的本質,屬于編譯器。在編譯的時候會在適當的地方給代碼添加retain,release,autorelese
NSString * str = @"Tom";
pthread_create(&pthread, NULL, func, (__bridge void *)(str));
//耗時操作函數
void * func(void * a){
//在這個函數中寫耗時操作
NSLog(@"%@-----%@",[NSThread currentThread],a);
return NULL;
}
打印輸出結果:
pThread
創建線程的基本使用就這么多,因為在 iOS 開發中幾乎不使用,其它的在這里也不多做介紹了;想了解其它的可以自己動手查一些相關的資料;
NSThread
NSThread
是經過蘋果封裝后的,并且完全面向對象的。因此可以直接操控線程對象,非常直觀和方便。但是,它的生命周期還是需要我們手動管理,所以這套方案也是偶爾用用,比如 [NSThread currentThread]
,它可以獲取當前線程類,你就可以知道當前線程的各種屬性,十分方便調試。
創建和啟動
//1.創建一個NSThread對象
NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(longTimeOperation) object:nil];
//2.開啟線程;線程一啟動就會在線程thread中執行self的longTimeOperation方法
[th start];
其它創建并啟動的方法
//創建線程后自動啟動線程
/**
參數:
1.需要在子線程中執行的操作的方法
2.來調用這個方法(子線程中執行的操作的方法)的對象
3.這個方法的參數
*/
[NSThread detachNewThreadSelector:@selector(run) toTarget:selfwithObject:nil];
//隱式創建并啟動線程
//將一個耗時操作放到后臺線程中(后臺線程也屬于子線程)
//后臺線程不是進程一開始運行的時候自動創建的線程,只是說我在這兒創建的子線程的優先級屬于后臺線程的優先級(也就是說創建一個優先級最低的線程)
[self performSelectorInBackground:@selector(run) withObject:nil];
//比較三種線程創建形式:
//后兩種是快速創建線程,優點是快速,不需要手動的開啟;缺點是不能拿到線程對象,沒有辦法對線程的屬性進行設置
//第一種的優點是可以拿到線程對象;缺點是需要手動開啟
其它方法和屬性
//主線程相關用法
+ (NSThread*)mainThread;//獲得主線程
- (BOOL)isMainThread;//是否為主線程
+ (BOOL)isMainThread;//是否為主線程
//線程的調度優先級
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
//調度優先級的取值范圍是0.0 ~1.0,默認0.5,值越大,優先級越高
//自己開發時,建議一般不要修改優先級
//線程的名字
- (void)setName:(NSString*)n;
- (NSString *)name;
//獲得當前線程
NSThread *current = [NSThread currentThread];
//使當前線程暫停一段時間,或者暫停到某個時刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;
//判斷某個線程的狀態的屬性
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
//線程的強制退出,取消;
+ (void)exit;
- (void)cancel;
線程的各種狀態的切換
(新建)、就緒、運行、阻塞、死亡
就緒:在可調度線程池里面,處于等待被調度的狀態(start)
運行:在可調度線程池里面,處于正在被調度的狀態(線程中的任務正在被執行)
阻塞:不在可調度線程池里,但是還存在內存中,處于暫停/休眠等待再次被喚醒(喚醒后會再次被添加到可調度線程池里面)
死亡:不在可調度線程池中,也不在內存中。不能再次添加到可調度線程池
GCD(Grand Central Dispatch)
全稱是Grand Central Dispatch,可譯為“牛逼的中樞調度器”
純C語言,提供了非常多強大的函數;不過由于使用了 Block,使得使用起來更加方便,而且靈活。
GCD的優勢:
- GCD是蘋果公司為多核的并行運算提出的解決方案,會自動利用更多的CPU內核(比如雙核、四核)
- GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程),更快的內存效率,因為線程棧不暫存與應用內存中;
- GCD提供了自動的和全面的線程池管理機制,穩定而便捷;提供了直接并且簡單的調用接口,使用方便準確.(程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼)。
任務和隊列
兩個核心概念:
1.任務:需要執行操作(想要做的事情)
2.隊列:用來存放任務
任務的執行分為:同步和異步兩種
隊列分為:串行隊列和并行隊列
同步:在當前線程中執行,不會開新線程;同時它會阻塞當前線程并等待 Block 中的任務執行完畢,然后當前線程才會繼續往下運行。
異步:會開新的線程,任務在新的線程中執行;當前線程會直接往下執行,它不會阻塞當前線程。
串行:順序,一個一個的執行(一個執行完才執行下一個)
并行:多個任務同時執行(可以將隊列中的任務全部取出來,只要線程就可以被執行)
GCD的使用的2個步驟
1.定制任務
確定想做的事情
2.將任務添加到隊列中
GCD會自動將隊列中的任務取出,放到對應的線程中執行
任務的取出遵循隊列的FIFO原則:先進先出,后進后出
GCD 將任務放到串行隊列會遵循隊列的FIFO原則取出來一個,執行一個,然后取下一個,這樣一個一個的執行。
GCD 將任務放到并行隊列的任務,GCD 也會 遵循隊列的FIFO原則取出來,但不同的是,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快,忽略不計,看起來,所有的任務都是一起執行的。不過需要注意,GCD 會根據系統資源控制并行的數量,所以如果任務很多,它并不會讓所有任務同時執行。
創建隊列
- 主隊列:主隊列是一個特殊的串行隊列。放在主隊列中的任務,都會放到主線程中執行;它主要用于刷新 UI\用戶的交互等,任何需要刷新 UI 的工作都要在主隊列執行,所以一般耗時任務都要放到子線程執行。
//獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
- 手動創建隊列:
//創建串行隊列
//參數:1.標簽(隊列的名字)2.隊列屬性(串行/并行)
//DISPATCH_QUEUE_SERIAL = NULL 串行
dispatch_queue_t queue = dispatch_queue_create("OneWang", NULL);
dispatch_queue_t queue = dispatch_queue_create("OneWang", DISPATCH_QUEUE_SERIAL);
dispatch_release(queue);//非ARC需要釋放手動創建的隊列
//創建并行隊列
//DISPATCH_QUEUE_CONCURRENT 并行
dispatch_queue_t queue = dispatch_queue_create("OneWang", DISPATCH_QUEUE_CONCURRENT);
- 全局并行隊列:GCD默認已經提供了全局的并發隊列,供整個應用使用,不需要手動創建;
//使用dispatch_get_global_queue函數獲得全局的并發隊列
//參數:1.隊列的優先級:一般使用默認的0 2.是蘋果預留的一個參數,暫時無用,用0即可
dispatch_queue_tdispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);
//獲得全局并發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue = dispatch_get_global_queue(0, 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 // 后臺
創建任務
//同步任務;它會阻塞當前線程并等待 Block 中的任務執行完畢,然后當前線程才會繼續往下運行。
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
//異步任務;任務在新的線程中執行;當前線程會直接往下執行,它不會阻塞當前線程。
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
//經典面試題:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(%"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
})
NSLog(@"3");
}
輸出結果想必大家應該都知道,最終只會輸出1;這是為什么呢?
因為打印完第一句后,dispatch_sync 立即阻塞當前的主線程,然后把 Block 中的任務放到
main_queue 中,可是 main_queue 中的任務會被取出來放到主線程中執行,但主線程這個時候
已經被阻塞了,所以 Block 中的任務就不能完成,它不完成,dispatch_sync 就會一直阻塞主
線程,這就是死鎖現象。導致主線程一直卡死。
暫停和繼續queue
我們可以使用dispatch_suspend
函數暫停一個queue以阻止它執行block對象;使用dispatch_resume
函數繼續dispatch queue。調用dispatch_suspend會增加queue的引用計數,調用dispatch_resume
則減少queue的引用計數。當引用計數大于0時,queue就保持掛起狀態。因此你必須對應地調用suspend和resume函數。掛起和繼續是異步的,而且只在執行block之間(比如在執行一個新的block之前或之后)生效。掛起一個queue不會導致正在執行的block停止。
隊列組
隊列組可以將很多隊列添加到一個組里,這樣做的好處是,當這個組里所有的任務都執行完了,隊列組會通過一個方法通知我們;
//創建一個隊列組
dispatch_group_t group = dispatch_group_create();
//將全局隊列中的耗時操作加隊列組中
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//執行1個耗時的異步操作
});
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//執行1個耗時的異步操作
});
//隊列組執行完畢的時候回自動通知
dispatch_group_notify(group,dispatch_get_main_queue(),
^{
//等前面的異步操作都執行完畢后,回到主線程...
});
線程安全問題dispatch_barrier_async
1.如果要防止資源的搶奪,得使用 synchronized 進行加鎖保護;
2.使用 GCD 的dispatch_barrier_async的作用便是在并發隊列中,完成在它之前提交到隊列中的任務后打斷,單獨執行其block。起到了一個線程鎖的作用。
- (void)dispatch_barrier{
dispatch_queue_t queue = dispatch_queue_create("com.meeting.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"前面的先執行,執行完畢后后面的才能執行");
});
dispatch_barrier_async(queue, ^{
NSLog(@"中間等待時間");
});
dispatch_async(queue, ^{
NSLog(@"前面的已經執行完畢,開始執行后面的");
});
dispatch_set_target_queue設置隊列的優先級
1.改變隊列的優先級;
//需求:生成一個后臺的串行隊列
- (void)changePriority{
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
//第一個參數:需要改變優先級的隊列;
//第二個參數:目標隊列;
//就是講第一個隊列的優先級指定為和第二個隊列的優先級相同;
//注意:第一個參數如果指定系統提供的Main Queue和Global Queue則不知道會發生什么情況,因此這些均不可以指定;
dispatch_set_target_queue(queue, bgQueue);
}
2.防止多個隊列的并發執行;
//多個串行隊列,設置了target queue
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t serial_queue_target = dispatch_queue_create("queue_target", NULL);
for (NSInteger index = 0; index < 5; index ++) {
//分別給每個隊列設置相同的target queue
dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", NULL);
dispatch_set_target_queue(serial_queue, serial_queue_target);
[array addObject:serial_queue];
}
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_async(queue, ^{
NSLog(@"任務%ld",idx);
});
}];
GCD 的底層實現原理概述:
GCD有一個底層線程池,這個池中存放的是一個個的線程。之所以稱為“池”,很容易理解出這個“池”中的線程是可以重用的,當一段時間后這個線程沒有被調用的話,這個線程就會被銷毀。注意:開多少條線程是由底層線程池決定的(線程建議控制再3~5條),池是系統自動來維護,不需要我們程序員來維護;我們只關心的是向隊列中添加任務,隊列調度即可。
iOS常見的延時執行有3種方式:
//調用NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
//2秒后再調用self的run方法
//使用GCD函數;需要注意的是dispatch_after僅表示在指定時間后提交任務,而非執行任務。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0* NSEC_PER_SEC)), dispatch_get_main_queue(),
^{
//2秒后異步執行這里的代碼...
});
//使用NSTimer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
GCD 定時器
平時我們所使用的 NSTimer 定時器容易受 RunLoop模式和線程的影響會導致定時器的計時不夠準確,這個時候我們可以使用 GCD 的定時器,它不受 RunLoop 的模式的影響,同時也是線程的安全的,因此 GCD 的定時器是最準確的;
/** 定時器(這里不用帶*,因為dispatch_source_t就是一個類,內部已經包含了*) */
@property (strong, nonatomic) dispatch_source_t timer;
//獲得隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//創建一個 GCD 的定時器dispatch_source_t本質還是 OC 對象
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設置定時器的各種屬性(什么時候開始任務,每隔多久執行一次)
//GCD 的時間參數,一般是納秒(1秒 == 10的9次方納秒)
//何時開始執行第一個任務
//dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC))比當前時間晚3秒
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (3.0 * NSEC_PER_SEC));
uint64_t interval = (2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
//設置回調
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"=========");
count ++;
if (count == 4) {
//取消定時器
dispatch_cancel(self.timer);
self.timer = nil;
}
});
//啟動定時器
dispatch_resume(self.timer);
單例模式
單例模式的作用
可以保證在程序運行過程,一個類只有一個實例,而且該實例易于供外界訪問從而方便地控制了實例個數,并節約系統資源;
單例模式的使用場合
在整個應用程序中,共享一份資源(這份資源只需要創建初始化1次);
//ARC中,單例模式的實現
//在.m中保留一個全局的static的實例
static id _instance;
//重寫allocWithZone:方法,在這里創建唯一的實例(注意線程安全)
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t
onceToken;dispatch_once(&onceToken,
^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
//提供1個類方法讓外界訪問唯一的實例
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken,
^{
_instance = [[self alloc] init];
});
return _instance;
}
//實現copyWithZone:方法
- (id)copyWithZone:(struct_NSZone *)zone
{
return _instance;
}
快速迭代
//使用dispatch_apply函數能進行快速迭代遍歷
dispatch_apply(10,dispatch_get_global_queue(0,0),^(size_t index){
//執行10次代碼,index順序不確定
});
信號量
信號量是一個整數,在創建的時候會有一個初始值,這個初始值往往代表我要控制的同時操作的并發數。在操作中,對信號量會有兩種操作:信號通知與等待。信號通知時,信號量會+1,等待時,如果信號量大于0,則會將信號量-1,否則,會等待直到信號量大于0。什么時候會大于零呢?往往是在之前某個操作結束后,我們發出信號通知,讓信號量+1;
我們來看看GCD中的三個信號量操作:
dispatch_semaphore_create:創建一個信號量(semaphore)
dispatch_semaphore_signal:信號通知,即讓信號量+1
dispatch_semaphore_wait:等待,直到信號量大于0時,即可操作,同時將信號量-1
在使用的時候,往往會創建一個信號量,然后進行多個操作,每次操作都等待信號量大于0再操作,同時信號昂-1,操作完后將信號量+1,類似下面這個過程:
dispatch_semaphore_t sema = dispatch_semaphore_create(5);
for (100次循環操作) {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 操作
dispatch_semaphore_signal(sema);
});
}
上面代碼表示我要操作100次,但是控制允許同時并發的操作最多只有5次,當并發量達到5后,信號量就減小到0了,這時候wait操作會起作用,DISPATCH_TIME_FOREVER表示會永遠等待,一直等到信號量大于0,也就是有操作完成了,將信號量+1了,這時候才可以結束等待,進行操作,并且將信號量-1,這樣新的任務又要等待。
多個請求結束后統一操作
假設我們一個頁面需要同時進行多個請求,他們之間倒是不要求順序關系,但是要求等他們都請求完畢了再進行界面刷新或者其他什么操作。
這個需求我們一般可以用GCD的group和notify來做到:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//請求1
NSLog(@"Request_1");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//請求2
NSLog(@"Request_2");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//請求3
NSLog(@"Request_3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任務均完成,刷新界面");
});
notify的作用就是在group中的其他操作全部完成后,再操作自己的內容,所以我們會看到上面三個內容都打印出來后,才打印界面刷新的內容。
但是當將上面三個操作改成真實的網絡操作后,這個簡單的做法會變得無效,為什么呢?因為網絡請求需要時間,而線程的執行并不會等待請求完成后才真正算作完成,而是只負責將請求發出去,線程就認為自己的任務算完成了,當三個請求都發送出去,就會執行notify中的內容,但請求結果返回的時間是不一定的,也就導致界面都刷新了,請求才返回,這就是無效的。
要解決這個問題,我們就要用到上面說的信號量來操作了。
在每個請求開始之前,我們創建一個信號量,初始為0,在請求操作之后,我們設一個dispatch_semaphore_wait,在請求到結果之后,再將信號量+1,也即是dispatch_semaphore_signal。這樣做的目的是保證在請求結果沒有返回之前,一直讓線程等待在那里,這樣一個線程的任務一直在等待,就不會算作完成,notify的內容也就不會執行了,直到每個請求的結果都返回了,線程任務才能夠結束,這時候notify也才能夠執行。偽代碼如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[網絡請求:{
成功:dispatch_semaphore_signal(sema);
失敗:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
多個請求順序執行
有時候我們需要按照順序執行多次請求,比如先請求到用戶信息,然后根據用戶信息中的內容去請求相關的數據,這在平常的代碼中直接按照順序往下寫代碼就可以了,但這里因為涉及到多線程之間的關系,就叫做線程依賴。
線程依賴用GCD做比較麻煩,建議用NSOperationQueue做,可以更加方便的設置任務之間的依賴。
// 1.任務一:獲取用戶信息
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A];
}];
// 2.任務二:請求相關數據
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];
// 3.設置依賴
[operation2 addDependency:operation1];// 任務二依賴任務一
// 4.創建隊列并加入任務
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation2, operation1] waitUntilFinished:NO];
一般的多線程操作這樣做是可以的,線程2會等待線程1完成后再執行。但是對于網絡請求,問題又來了,同樣,網絡請求需要時間,線程發出請求后即認為任務完成了,并不會等待返回后的操作,這就失去了意義。
要解決這個問題,還是用信號量來控制,其實是一個道理,代碼也是一樣的,在一個任務操作中;還是去等待請求返回后,才讓任務結束。而依賴關系則通過NSOperationQueue來實現。
總結
其實歸根結底,中心思想就是通過信號量,來控制線程任務什么時候算作結束,如果不用信號量,請求發出后即認為任務完成,而網絡請求又要不同時間,所以會打亂順序。因此用一個信號量來控制在單個線程操作內,必須等待請求返回,自己要執行的操作完成后,才將信號量+1,這時候一直處于等待的代碼也得以執行通過,任務才算作完成。
通過這個方法,就可以解決由于網絡請求耗時特性而帶來的一些意想不到的多線程處理的問題。
NSOperation & NSOperationQueue
NSOperation 是蘋果公司對 GCD 的封裝,完全面向對象,所以使用起來更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分別對應 GCD 的 任務 和 隊列 :
NSOperation和NSOperationQueue實現多線程的具體步驟:
1.先將需要執行的操作封裝到一個NSOperation對象中
2.然后將NSOperation對象添加到NSOperationQueue中
3.系統會自動將NSOperationQueue中的NSOperation取出來
4.將取出的NSOperation封裝的操作放到一條新線程中執行
NSOperation的子類
NSOperation是個抽象類,并不具備封裝操作的能力,必須使用它的子類;
使用NSOperation子類的方式有3種:
NSInvocationOperation
NSBlockOperation
自定義子類繼承NSOperation,實現內部相應的方法
添加任務
1.創建NSInvocationOperation對象
- -(id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
調用start方法開始執行操作 - -(void)start;
一旦執行操作,就會調用target的sel方法
注意 - 默認情況下,調用了start方法后并不會開一條新線程去執行操作,而是在當前線程同步執行操作
- 只有將NSOperation放到一個NSOperationQueue中,才會異步執行操作
2.創建NSBlockOperation對象
- +(id)blockOperationWithBlock:(void(^)(void))block;
通過addExecutionBlock:方法添加更多的操作
- -(void)addExecutionBlock:(void(^)(void))block;
注意:只要NSBlockOperation封裝的操作數 >1,就會異步執行操作
3.自定義Operation
如果NSInvocationOperation和NSBlockOperation對象不能滿足需求, 你可以直接繼承NSOperation, 并添加任何你想要的行為。繼承所需的工作量主要取決于你要實現非并發還是并發的NSOperation。定義非并發的NSOperation要簡單許多,只需要重載-(void)main這個方法,在這個方法里面執行主任務,并正確地響應取消事件; 對于并發NSOperation, 你必須重寫NSOperation的多個基本方法進行實現(這里暫時先介紹非并發的NSOperation);
自定義NSOperation的步驟
1.重寫- (void)main方法,在里面實現想執行的任務
2.重寫- (void)main方法的注意點
- 自己創建自動釋放池(因為如果是異步操作,無法訪問主線程的自動釋放池)
- 經常通過- (BOOL)isCancelled方法檢測操作是否被取消,對取消做出響應
operation開始執行之后,會一直執行任務直到完成,或者顯式地取消操作。取消可能發生在任何時候,甚至在operation執行之前。盡管NSOperation提供了一個方法,讓應用取消一個操作,但是識別出取消事件則是我們自己的事情。如果operation直接終止, 可能無法回收所有已分配的內存或資源。因此operation對象需要檢測取消事件,并優雅地退出執行
NSOperation對象需要定期地調用isCancelled方法檢測操作是否已經被取消,如果返回YES(表示已取消),則立即退出執行。不管是自定義NSOperation子類,還是使用系統提供的兩個具體子類,都需要支持取消。isCancelled方法本身非常輕量,可以頻繁地調用而不產生大的性能損失
以下地方可能需要調用isCancelled:
- 在執行任何實際的工作之前
- 在循環的每次迭代過程中,如果每個迭代相對較長可能需要調用多次
- 代碼中相對比較容易中止操作的任何地方
這個目前只了解這些,以后研究過再來更新吧!
//1.將操作封裝到NSOperation的子類中
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(longTimeOperation:) object:@"Invocation"];
//2.開始執行操作
//不會開新的線程
[operation start];
//1.操作
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
//在這兒寫耗時操作
}];
//添加多個Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//2.開始執行操作
//不會開新的線程
[operation start];
創建隊列
NSOperationQueue的作用
NSOperation可以調用start方法來執行任務,但默認是同步執行的;
如果將NSOperation添加到NSOperationQueue(操作隊列)中,系統會自動異步執行NSOperation中的操作;
添加操作到NSOperationQueue中
- -(void)addOperation:(NSOperation*)operation;
- -(void)addOperationWithBlock:(void(^)(void))block;
最大并發數的相關方法
- -(NSInteger)maxConcurrentOperationCount;
- -(void)setMaxConcurrentOperationCount:(NSInteger)cnt;
取消隊列的所有操作
- -(void)cancelAllOperations;
提示:也可以調用NSOperation的- (void)cancel方法取消單個操作
暫停和恢復隊列 - -(void)setSuspended:(BOOL)b;//YES代表暫停隊列,NO代表恢復隊列
- -(BOOL)isSuspended;
操作優先級
設置NSOperation在queue中的優先級,可以改變操作的執行優先級
- -(NSOperationQueuePriority)queuePriority;
- -(void)setQueuePriority:(NSOperationQueuePriority)p;
優先級的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
操作的監聽
可以通過 KVO監聽一個操作的執行完畢
- -(void(^)(void))completionBlock;
- -(void)setCompletionBlock:(void(^)(void))block;
操作的依賴關系
NSOperation之間可以設置依賴來保證執行順序
依賴約束的是結束時刻;
比如一定要讓操作A執行完后,才能執行操作B,可以這么寫
[operationB addDependency:operationA];// 操作B依賴于操作A
可以在不同queue的NSOperation之間創建依賴關系
使用依賴關系有三點需要注意:
1.不要建立循環依賴,會造成死鎖,原因和循環引用是一樣的;可以使用 removeDependency
來解除依賴關系;
2.使用依賴建立使用 NSIncocationOperation, NSIncocationOperation和 NSBlockOperation 混用會導致依賴關系無法正常實現;
3.依賴關系不光在同隊列中生效,不同隊列的NSOperation對象之間設置的依賴關系一樣會生效;
GCD 和 NSOperationQueue 的區別:
- GCD 是純 C 語言的 API, NSOperationQueue是基于 GCD 的 OC 版本封裝;
- GCD 只支持 FIFO 隊列, NSOperationQueue可以很方便的設置執行順序,設置最大并發數;
- NSOperationQueue可以在 operation 之間設置依賴關系,而 GCD 需要寫很多 代碼才能實現;
- NSOperationQueue支持 KVO, 可以檢測 operation;
- GCD 的執行速度比NSOperationQueue塊;