在軟體開發中,多線程程式設計技術被廣泛應用,相信多線程任務對我們來說已經不再陌生了。有了多線程技術,我們可以同做多個事情,而不是一個一個任務地進行。比如:前端和背景作互動、大任務(需要耗費一定的時間和資源)等等。也就是說,我們可以使用線程把占據時間長的任務放到背景中處理,而不影響到使用者的使用。
簡介
線程間通訊
有一個非常重要的隊列,就是主隊列。在這個隊列中處理多點觸控及所有與UI相關操作等等。它非常特殊,原因有兩點。一是我們絕對不想它阻塞,我們不會将需要執行很長時間的任務放在主隊列上執行。二是我們将其用于所有與UI相關的同步,也就是線程間通訊需要注意的地方。所有有可能會使螢幕UI發生變化的,都應放在主隊列上執行。
線程的定義:
每個正在系統上運作的程式都是一個程序。每個程序包含一到多個線程。程序也可能是整個程式或者是部分程式的動态執行。線程是一組指令的集合,或者是程式的特殊段,它可以在程式裡獨立執行。也可以把它了解為代碼運作的上下文。是以線程基本上是輕量級的程序,它負責在單個程式裡執行多任務。通常由作業系統負責多個線程的排程和執行。
轉自百度百科:多線程
如果熟悉多線程程式設計技術這一塊的朋友們,可以去看關于多線程安全的文章,是我寫的另一篇文章”iOS開發-多線程開發之線程安全篇“;
IOS支援的多線程技術:
一、Thread:
1)顯式建立線程:NSThreed
2)隐式建立線程:NSObject
二、Cocoa operations:
NSOperation類是一個抽象類,因為我們必須使用它的兩個子類。
1)NSInvocationOperation
2)NSBlockOperation
————————————————————————————
3)NSOperationQueue(繼承于NSObject)
三、Grand Central Dispatch (GCD):
1)GCD的建立
2)重複執行線程及一次性執行:dispatch_apply & dispatch_once
3)操作(串行)隊列:dispatch_queue_create
4)GCD群組通知:dispatch_group_t
5)GCD實作計時器
6)背景運作
7)延遲執行
四、比較多線程技術
一、Thread
我們可以使用NSTherad或NSObject類去調用:
1)顯式建立線程:NSThread
建立NSThread有兩個辦法
1.1)建立之後需要使用start方法,才會執行方法:
NSThread *threadAlloc = [[NSThread alloc] initWithTarget:self selector:@selector(threadAlloc) object:nil];
[threadAlloc start];
1.2)建立并馬上執行方法:
[NSThread detachNewThreadSelector:@selector(threadAlloc:) toTarget:self withObject:nil];
我們也可以使用NSObject類的方法直接調用方法
[self performSelectorInBackground:@selector(threadAlloc) withObject:nil];
取消線程的方法:
實際上并沒有真正提供取消線程的API。蘋果提供了一個cancel的api,但它不能作用于取消線程,它隻能改變線程的運作狀态。我們可以使用它來進行條件判斷。
- (void)threadCancel
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadCancelNow) object:nil];
[thread start];
}
- (void)threadCancelNow
{
int a = 0;
while (![[NSThread currentThread] isCancelled]) {
NSLog(@"a - %d", a);
a++;
if (a == 5000) {
NSLog(@"終止循環");
[[NSThread currentThread] cancel];
break;
}
}
}
程式效果:循環輸出5000次,線程就會被終止。
NSThread線程間通訊-調用主線程修改UI:
隻需要傳遞一個selector和它的參數,withObject參數可以為nil,waitUntilDone代表是否要等待調用它的這個線程執行之後再将它從主隊列調出,并在主隊列上運作,通常設為NO,不需要等待。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
NSThread相關屬性及方法:
// 擷取/設定線程的名字
@property (copy) NSString *name NS_AVAILABLE(10_5, 2_0);
/**
* 擷取目前線程的線程對象
*
* 通過這個屬性可以檢視目前線程是第幾條線程,主線程為1。
* 可以看到目前線程的序号及名字,主線程的序号為1,依次疊加。
*/
+ (NSThread *)currentThread;
// 線程休眠(秒)
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 線程休眠,指定具體什麼時間休眠
+ (void)sleepUntilDate:(NSDate *)date;
// 退出線程
// 注意:這裡會把線程對象銷毀!銷毀後就不能再次啟動線程,否則程式會崩潰。
+ (void)exit;
二、Cocoa operations
1)NSInvocationOperation
建立NSInvocationOperation線程,附帶一個NSString參數:
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"abc"];
// 需要啟動線程,預設是不啟動的。
[operation start];
如在建立時定義了參數,那麼接收的時候,可以對sender進行轉換,如字元串、數組等:
- (void)invocationAction:(NSInvocationOperation *)sender
{
NSLog(@"sender - %@", sender); // 輸出params
NSString *str = (NSString *)sender;
NSLog(@"str - %@e", str); // params
}
附帶一提,線程的普通建立一般為并發執行的,因為串行隊列是需要顯式建立的,如沒看見此類代碼,那麼即是并發隊列線程,是以,上述代碼也就是并發線程。關于并發和串行隊列(線程),我将會在下面詳細說明,我們繼續往下看。
你也可以使用NSOperationQueue來建立一個線程隊列,用來添加子線程:
NSOperationQueue *invocationQueue = [[NSOperationQueue alloc] init];
// 線程A
NSInvocationOperation *invocationQ1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"invocationQ1"];
// 線程B
NSInvocationOperation *invocationQ2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"invocationQ2"];
// 往invocationQueue添加子線程
[invocationQueue addOperations:@[invocationQ1, invocationQ2] waitUntilFinished:YES];
必須使用addOperations:方法把線程添加至隊列,不然線程不會執行,隊列是并行執行。或者,你也可以使用addOperation:方法添加單個線程。
建立NSBlockOperation
// 建立線程任務
NSBlockOperation *blockOperation = [NSBlockOperation
blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"one - %@", [NSThread currentThread]);
}];;// 執行線程任務
[blockOperation start];
注意:這會在目前的線程中執行,因為它是根據調用的線程所決定的。
比方說你在主線程中運作它,那麼它就是在主線程中執行任務。如果你是在子線程中運作它,那麼它就是在子線程中執行任務。
做個簡單的實驗,我們建立一條子線程,然後在子線程裡調用NSBlockOperation
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSBlockOperation *blockOperation = [NSBlockOperation
blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"one - %@", [NSThread currentThread]);
// print: one - <NSThread: 0x7f8ac2e1d0b0>{number = 2, name = (null)}
}];;
[blockOperation start];
});
它将列印:one - <NSThread: 0x7f8ac2e1d0b0>{number = 2, name = (null)},是以這個理論是正确的
我們也可以使它并發執行,通過使用addExecutionBlock方法添加多個Block,這樣就能使它在主線程和其它子線程中工作。
NSBlockOperation *blockOperation = [NSBlockOperation
blockOperationWithBlock:^{
NSLog(@"one - %@", [NSThread currentThread]);
}];;
[blockOperation addExecutionBlock:^{
NSLog(@"two - %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"three - %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"four - %@", [NSThread currentThread]);
}];
[blockOperation start];
它将列印:
two - <NSThread: 0x7fea8a70b000>{number = 3, name = (null)}
one - <NSThread: 0x7fea8a558a40>{number = 4, name = (null)}
four - <NSThread: 0x7fea8a406b90>{number = 1, name = main}
three - <NSThread: 0x7fea8a436e40>{number = 2, name = (null)}
大家都看到,即使我們通過使用addExecutionBlock方法使它并發執行任務,但是它也依舊會在主線程執行,是以我們就需要使用NSOperationQueue了。
3)NSOperationQueue
這裡介紹一下NSOperation的依賴關系,依賴關系會影響線程的執行順序:
// 建立操作隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 線程A
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1");
[NSThread sleepForTimeInterval:2];
}];
// 線程B
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op2");
}];
// 線程C
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op3");
[NSThread sleepForTimeInterval:2];
}];
// 線程B依賴線程C,也就是等線程C執行完之後才會執行線程B
[op2 addDependency:op3];
// 線程C依賴線程A,同上,隻不過依賴對象改成了線程A
[op3 addDependency:op1];
// 為隊列添加線程
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
當你沒添加依賴時,隊列是并行執行的。
注意:依賴關系可以多重依賴,但不要建立循環依賴。
Cocoa operations線程間通信-調用主線程修改UI:
// 建立線程對象(并發)
NSBlockOperation *blockOperation = [[NSBlockOperation alloc] init];
// 添加新的操作
[blockOperation addExecutionBlock:^{
NSLog(@"two - %@", [NSThread currentThread]);
}];
// 添加新的操作
[blockOperation addExecutionBlock:^{
NSLog(@"three - %@", [NSThread currentThread]);
// 在主線程修改UI
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperationWithBlock:^{
[self editUINow];
}];
}];
[blockOperation start];
NSOperation方法及屬性:
// 設定線程的最大并發數
@property NSInteger maxConcurrentOperationCount;
// 線程完成後調用的Block
@property (copy) void (^completionBlock)(void);
// 取消線程
- (void)cancel;
隻列舉上面那些,其它的方法就不全列出來了。
注意:在NSOperationQueue類中,我們可以使用cancelAllOperations方法取消所有的線程。這裡需要說明一下,不是執行cancelAllOperations方法時就會馬上取消,是等目前隊列執行完,下面的隊列不會再執行。
三、Grand Central Dispatch (GCD)
1)GCD異步線程的建立:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程 - %@", [NSThread currentThread]);
});
GCD也可以建立同步的線程,隻需要把async改成sync即可。
2)重複執行線程:dispatch_apply
以下代碼會執行4次:
dispatch_apply(4, DISPATCH_QUEUE_PRIORITY_DEFAULT, ^(size_t index) {
// index則為執行的次數 0開始遞增
NSLog(@"one - %ld", index);
});
index參數為執行的次數,從0開始遞增。
其中需要注意的是,每次執行都會新開辟一條子線程,因為是異步的原因,它們不會是順序的。
[657:159159] one - 0, thread - <NSThread: 0x100110b50>{number = 1, name = main}
[657:159191] one - 2, thread - <NSThread: 0x103800000>{number = 2, name = (null)}
[657:159192] one - 3, thread - <NSThread: 0x100112b90>{number = 3, name = (null)}
[657:159190] one - 1, thread - <NSThread: 0x100501180>{number = 4, name = (null)}
然而,GCD還有一次性執行的方法:
dispatch_once_t once;
dispatch_once(&once, ^{
NSLog(@"once - %@", [NSThread currentThread]); // 主線程
});
它通常用于建立單例。
3)操作隊列:dispatch_queue_create
使用GCD也能建立串行隊列,具體代碼如下:
/**
* GCD建立串行隊列
*
* @param "com.GarveyCalvin.queue" 隊列字元串辨別
* @param DISPATCH_QUEUE_CONCURRENT 可選的,可以是NULL
*
* @return dispatch_queue_t
*/
dispatch_queue_t queue = dispatch_queue_create("com.GarveyCalvin.queue", DISPATCH_QUEUE_CONCURRENT);
// 線程A
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"sleep async - %@", [NSThread currentThread]);
});
// 線程B
dispatch_barrier_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"sleep barrier2 - %@", [NSThread currentThread]);
});
// 線程C
dispatch_async(queue, ^{
NSLog(@"async");
});
運作效果:以上會先執行 線程A-》線程B-》線程C,它是一個串行隊列。
dispatch_queue_create的第二個參數:
1)DISPATCH_QUEUE_SERIAL(串行)
2)DISPATCH_QUEUE_CONCURRENT(并發)
GCD的進階用法,等所有線程都完成工作後,再作通知。
// 建立群組
dispatch_group_t group = dispatch_group_create();
// 線程A
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"group1");
[NSThread sleepForTimeInterval:2];
});
// 線程B
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"group2");
});
// 待群組裡的線程都完成之後調用的通知
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"group success");
});
群組裡的線程也是并行隊列。線程A和線程B都執行完之後,會調用通知列印group success。
__block int time = 30;
CGFloat reSecond = 1.0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, reSecond * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
time--;
NSLog(@"%d", time);
if (time == 0) {
dispatch_source_cancel(timer);
}
});
dispatch_resume(timer);
代碼效果:建立了一個計時器,計時器運作30秒,每過一秒會調用一次block,我們可以在block裡面寫代碼。因為dispatch_source_t預設是挂起狀态,是以我們使用時需要使用dispatch_resume方法先恢複,不然線程不會執行。
GCD線程間通信-調用主線程修改UI:
有時候我們請求背景作資料處理,資料處理是異步的,資料處理完成後需要更新UI,這時候我們需要切換到主線程修改UI,例子如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"異步資料處理 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
NSLog(@"資料處理完成");
// 調用主線程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"更新UI - %@", [NSThread currentThread]);
[self editUINow];
});
});
因為是在主線程修改UI,是以我們最好是使用同步的GCD方法dispatch_sync。但這還不夠,我們還需要使用dispatch_get_main_queue()方法來獲得主線程,之後就是作UI的更新工作了。
GCD方法及屬性:
// 擷取主線程
dispatch_get_main_queue()
// 建立隊列:第一個參數是隊列的名稱,它會出現在調試程式等之中,是個内部名稱。第二個參數代表它是串行隊列還是并并發隊列,NULL代表串行隊列。
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 建立異步排程隊列
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
// 恢複隊列
void dispatch_resume(dispatch_object_t object);
// 暫停隊列
void dispatch_suspend(dispatch_object_t object);
小結:本文主要介紹了IOS三種線程對比及其使用方法。需要特别注意的是,在修改任何有關于UI的東西,我們必須要切換至主線程,在主線程裡修改UI,避免不必要的麻煩産生。蘋果是推薦我們使用GCD,因為GCD是這三種裡面抽象級最高的,使用起來也簡單,也是消耗資源最低的,并且它執行效率比其它兩種都高。是以,能夠使用GCD的地方,盡量使用GCD。
使用block的另一個好處是可以讓程式在背景較久地運作。在以前,當應用被按Home鍵退出後,應用僅有最多5秒的時間做一些儲存或清理資源的工作。 但是如果使用GCD,你可以讓你的應用最多有10分鐘的時間在背景長久運作。這個時間可以用來做各種事情,包括清理本地緩存、發送統計資料等工作。
AppDelegate.h
@interface AppDelegate ()
@property (assign, nonatomic) UIBackgroundTaskIdentifier backGroundUpdate;
@end
AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application {
[self beginBackGroundUpdate];
// 需要長久運作的代碼
[self endBackGroundUpdate];
}
- (void)beginBackGroundUpdate
{
self.backGroundUpdate = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackGroundUpdate];
}];
}
- (void)endBackGroundUpdate
{
[[UIApplication sharedApplication] endBackgroundTask:self.backGroundUpdate];
self.backGroundUpdate = UIBackgroundTaskInvalid;
}
建議大家在真機上測試,因為筆者在模拟器測試了24分鐘還有效。
如果我們想要某段代碼延遲執行,那麼可以使用dispatch_after ,但是有一個缺點是,當送出代碼後(代碼執行後),我們不能取消它,它将會運作。另外,我們可以使用 NSTimer 進行延時操作,值得一提,它是可以被取消的。
dispatch_time_t time_t = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC));
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(time_t, queue, ^{
NSLog(@"hahalo");
});
比較多線程技術
一、Thread:
優點:量級較輕。
缺點:需要自己管理線程的生命周期,線程同步。線程同步對資料的加鎖會有一定的系統開銷。
優點:不需要關心線程管理,資料同步的事情,可以把精力放在自己需要執行的操作上。
優點:GCD基于C的API,非常底層,可以充分利用多核,能夠輕松在多核系統上高效運作并發代碼,也是蘋果推薦使用的多線程技術。
本文參考:
iOS多線程開發
GCD的另一個用處是可以讓程式在背景較長久的運作。
全面掌握iOS多線程攻略 —— PS:這個攻略較多,但是有很多重複的内容。
iOS多線程的初步研究(一)-- NSThread
博文作者:GarveyCalvin
博文出處:http://www.cnblogs.com/GarveyCalvin/
本文版權歸作者和部落格園共有,歡迎轉載,但須保留此段聲明,并給出原文連結,謝謝合作!
活着,就是為了改變世界!