多線程問題在iOS中目前有4
套多線程方案秘蛔,他們分別是:
- Pthreads
- NSthread
- GCD
- NSOperation
Pthreads
這是一套在很多操作系統(tǒng)通用的多線程API呢袱,不過這是基于c語言的框架,在iOS實際開發(fā)中很少使用饱亿。
- 包含頭文件:
#import <pthread.h>
- 創(chuàng)建線程,并執(zhí)行任務(wù):
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 創(chuàng)建一個新的線程并自動執(zhí)行
pthread_t thread;
pthread_create(&thread, NULL, pthreadStart, NULL);
}
void *pthreadStart() {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
- 輸出打印:
<NSThread: 0x6040002719c0>{number = 3, name = (null)}
NSthread
這套方案是蘋果封裝的多線程解決方案滩租。可以直接操控線程對象利朵。但是律想,它的生命周期還是需要我們手動管理,所以這套方案使用頻率不高绍弟,下面來看看它的一些用法技即。
- 創(chuàng)建線程并啟動
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
[thread start];
- 創(chuàng)建線程并自動啟動
[NSThread detachNewThreadSelector:@selector(threadStart) toTarget:self withObject:nil];
- 使用NSObject的方法創(chuàng)建線程并自動啟動
[self performSelectorInBackground:@selector(threadStart) withObject:nil];
- 其他常用方法
//取消線程
- (void)cancel;
//啟動線程
- (void)start;
//判斷某個線程的狀態(tài)的屬性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
//設(shè)置和獲取線程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
//獲取當(dāng)前線程信息
+ (NSThread *)currentThread;
//獲取主線程信息
+ (NSThread *)mainThread;
//使當(dāng)前線程暫停一段時間,或者暫停到某個時刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;
GCD
Grand Central Dispatch
樟遣,它是蘋果為多核的并行運算提出的解決方案,所以會自動合理地利用更多的CPU內(nèi)核(比如雙核、四核)抽莱,最重要的是它會自動管理線程的生命周期(創(chuàng)建線程分瘦、調(diào)度任務(wù)、銷毀線程)瞻佛,完全不需要我們管理脱篙,我們只需要告訴干什么就行娇钱。同時它使用的也是 c語言,不過由于使用了Block
(Swift里叫做閉包)绊困,使得使用起來更加方便文搂,而且靈活。
1. 任務(wù)和隊列
在GCD
中秤朗,加入了兩個非常重要的概念:任務(wù)和隊列煤蹭。
- 任務(wù):即操作,你想要干什么川梅,說白了就是一段代碼疯兼,在 GCD 中就是一個 Block,所以添加任務(wù)十分方便贫途。任務(wù)有兩種執(zhí)行方式: 同步執(zhí)行 和 異步執(zhí)行吧彪,他們之間的區(qū)別是 是否會創(chuàng)建新的線程。
同步(sync) 和 異步(async) 的主要區(qū)別在于會不會阻塞當(dāng)前線程丢早,直到 Block 中的任務(wù)執(zhí)行完畢姨裸!
如果是 同步(sync) 操作,它會阻塞當(dāng)前線程并等待 Block 中的任務(wù)執(zhí)行完畢怨酝,然后當(dāng)前線程才會繼續(xù)往下運行傀缩。
如果是 異步(async)操作,當(dāng)前線程會直接往下執(zhí)行农猬,它不會阻塞當(dāng)前線程赡艰。
- 隊列:用于存放任務(wù)。一共有兩種隊列斤葱, 串行隊列 和 并行隊列慷垮。
串行隊列 中的任務(wù),GCD 會 FIFO(先進(jìn)先出) 地取出來一個揍堕,執(zhí)行一個料身,然后取下一個,這樣一個一個的執(zhí)行衩茸。
并行隊列 中的任務(wù)芹血,GCD 也會 FIFO的取出來,但不同的是楞慈,它取出來一個就會放到別的線程幔烛,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快囊蓝,忽略不計饿悬,看起來,所有的任務(wù)都是一起執(zhí)行的慎颗。不過需要注意乡恕,GCD 會根據(jù)系統(tǒng)資源控制并行的數(shù)量言询,所以如果任務(wù)很多,它并不會讓所有任務(wù)同時執(zhí)行傲宜。
2. 創(chuàng)建隊列
- 主隊列:這是一個特殊的 串行隊列运杭。什么是主隊列,大家都知道吧函卒,它用于刷新 UI辆憔,任何需要刷新 UI 的工作都要在主隊列執(zhí)行,所以一般耗時的任務(wù)都要放到別的線程執(zhí)行报嵌。
//OBJECTIVE-C
dispatch_queue_t queue = ispatch_get_main_queue();
-
自己創(chuàng)建的隊列:自己可以創(chuàng)建
串行隊列
, 也可以創(chuàng)建并行隊列
虱咧。看下面的代碼(代碼已更新)锚国,它有兩個參數(shù)腕巡,第一個上面已經(jīng)說了,第二個才是最重要的血筑。
第二個參數(shù)用來表示創(chuàng)建的隊列是串行的還是并行的绘沉,傳入DISPATCH_QUEUE_SERIAL
或NULL
表示創(chuàng)建串行隊列。傳入DISPATCH_QUEUE_CONCURRENT
表示創(chuàng)建并行隊列豺总。
//串行隊列
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
//并行隊列
dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
- 全局并行隊列:只要是并行任務(wù)一般都加入到這個隊列车伞。這是系統(tǒng)提供的一個并發(fā)隊列。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3. 創(chuàng)建任務(wù)
- 同步任務(wù):會阻塞當(dāng)前線程 (SYNC)
dispatch_sync(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
- 異步任務(wù):不會阻塞當(dāng)前線程 (ASYNC)
dispatch_async(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
4. 隊列組
隊列組可以將很多隊列添加到一個組里喻喳,這樣做的好處是另玖,當(dāng)這個組里所有的任務(wù)都執(zhí)行完了,隊列組會通過一個方法通知我們表伦。下面是使用方法谦去,這是一個很實用的功能。
//1.創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
//2.創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.多次使用隊列組的方法執(zhí)行任務(wù), 只有異步方法
//3.1.執(zhí)行3次循環(huán)
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
//3.2.主隊列執(zhí)行8次循環(huán)
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 8; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});
//3.3.執(zhí)行5次循環(huán)
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
//4.都完成后會自動通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成 - %@", [NSThread currentThread]);
});
5. 柵欄方法
- 我們有時需要異步執(zhí)行兩組操作绑榴,而且第一組操作執(zhí)行完之后哪轿,才能開始執(zhí)行第二組操作盈魁。這樣我們就需要一個相當(dāng)于柵欄一樣的一個方法將兩組異步執(zhí)行的操作組給分割起來翔怎,當(dāng)然這里的操作組里可以包含一個或多個任務(wù)。這就需要用到
dispatch_barrier_async
方法在兩個操作組間形成柵欄杨耙。
dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"----1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----2-----%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----3-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----4-----%@", [NSThread currentThread]);
});
6. 延時執(zhí)行
- 當(dāng)我們需要延遲執(zhí)行一段代碼時赤套,就需要用到GCD的
dispatch_after
方法。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后異步執(zhí)行這里的代碼...
NSLog(@"run-----");
});
7. 一次性代碼
- 我們在創(chuàng)建單例珊膜、或者有整個程序運行過程中只執(zhí)行一次的代碼時容握,我們就用到了GCD的
dispatch_once
方法。使用dispatch_once
函數(shù)能保證某段代碼在程序運行過程中只被執(zhí)行1次车柠。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執(zhí)行1次的代碼(這里面默認(rèn)是線程安全的)
});
8. 快速迭代方法
- 通常我們會用for循環(huán)遍歷剔氏,但是GCD給我們提供了快速迭代的方法
dispatch_apply
塑猖,使我們可以同時遍歷。比如說遍歷0~5這6個數(shù)字谈跛,for循環(huán)的做法是每次取出一個元素羊苟,逐個遍歷。dispatch_apply
可以同時遍歷多個數(shù)字感憾。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd------%@",index, [NSThread currentThread]);
});
NSOperation
NSOperation 是蘋果公司對 GCD 的封裝蜡励,完全面向?qū)ο螅允褂闷饋砀美斫狻?大家可以看到NSOperation
和NSOperationQueue
分別對應(yīng) GCD 的任務(wù)
和隊列
阻桅。操作步驟如下:
- 將要執(zhí)行的任務(wù)封裝到一個
NSOperation
對象中凉倚。 - 將此任務(wù)添加到一個
NSOperationQueue
對象中。
注:
NSOperation
只是一個抽象類嫂沉,所以不能封裝任務(wù)稽寒。但它有 2 個子類用于封裝任務(wù)。分別是:NSInvocationOperation
和NSBlockOperation
趟章。創(chuàng)建一個Operation
后瓦胎,需要調(diào)用start
方法來啟動任務(wù),它會 默認(rèn)在當(dāng)前隊列同步執(zhí)行尤揣。當(dāng)然你也可以在中途取消一個任務(wù)搔啊,只需要調(diào)用其cancel
方法即可。
- NSInvocationOperation:
//1.創(chuàng)建NSInvocationOperation對象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
//2.開始執(zhí)行
[operation start];
- NSBlockOperation
//1.創(chuàng)建NSBlockOperation對象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//2.開始任務(wù)
[operation start];
NSBlockOperation
默認(rèn)會在當(dāng)前線程執(zhí)行北戏。但它還有一個方法:addExecutionBlock:
负芋,通過這個方法可以給Operation
添加多個執(zhí)行 Block。這樣 Operation 中的任務(wù) 會并發(fā)執(zhí)行嗜愈,它會在主線程和其它的多個線程 執(zhí)行這些任務(wù)旧蛾。
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"execution - %@", [NSThread currentThread]);
}];
}
[operation start];
輸出打印:
<NSThread: 0x60000046c880>{number = 9, name = (null)}
execution - <NSThread: 0x604000078ac0>{number = 1, name = main}
execution - <NSThread: 0x600000469280>{number = 11, name = (null)}
execution - <NSThread: 0x60400026cb40>{number = 10, name = (null)}
execution - <NSThread: 0x60000046c880>{number = 9, name = (null)}
execution - <NSThread: 0x604000078ac0>{number = 1, name = main}
- 自定義Operation
除了上面的兩種Operation
以外蠕嫁,我們還可以自定義Operation
锨天。自定義Operation
需要繼承NSOperation
類,并實現(xiàn)其main()
方法剃毒,因為在調(diào)用start()
方法的時候病袄,內(nèi)部會調(diào)用main()
方法完成相關(guān)邏輯。所以如果以上的兩個類無法滿足你的欲望的時候赘阀,你就需要自定義了益缠。你想要實現(xiàn)什么功能都可以寫在里面。除此之外基公,你還需要實現(xiàn)cancel()
在內(nèi)的各種方法幅慌。
創(chuàng)建隊列
看過上面的內(nèi)容就知道,我們可以調(diào)用一個NSOperation
對象的start()
方法來啟動這個任務(wù)轰豆,但是這樣做他們默認(rèn)是 同步執(zhí)行 的胰伍。就算是addExecutionBlock
方法齿诞,也會在 當(dāng)前線程和其他線程 中執(zhí)行,也就是說還是會占用當(dāng)前線程骂租。這是就要用到隊列NSOperationQueue
了掌挚。而且,按類型來說的話一共有兩種類型:主隊列菩咨、其他隊列吠式。只要添加到隊列,會自動調(diào)用任務(wù)的start()
方法抽米。
- 主隊列
細(xì)心的同學(xué)就會發(fā)現(xiàn)特占,每套多線程方案都會有一個主線程(當(dāng)然啦,說的是iOS中云茸,像pthread
這種多系統(tǒng)的方案并沒有是目,因為UI線程理論需要每種操作系統(tǒng)自己定制)。這是一個特殊的線程标捺,必須串行懊纳。所以添加到主隊列的任務(wù)都會一個接一個地排著隊在主線程處理。
NSOperationQueue *queue = [NSOperationQueue mainQueue];
- 其他隊列
因為主隊列比較特殊亡容,所以會單獨有一個類方法來獲得主隊列嗤疯。那么通過初始化產(chǎn)生的隊列就是其他隊列了,因為只有這兩種隊列闺兢,除了主隊列茂缚,其他隊列就不需要名字了。
注意:其他隊列的任務(wù)會在其他線程并行執(zhí)行屋谭。
//1.創(chuàng)建一個其他隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//2.創(chuàng)建NSBlockOperation對象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//3.添加多個Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//4.隊列添加任務(wù)
[queue addOperation:operation];
打印輸出
2015-07-28 20:26:28.463 test[18622:4443534] <NSThread: 0x7fd022c3ac10>{number = 5, name = (null)}
2015-07-28 20:26:28.463 test[18622:4443536] 第2次 - <NSThread: 0x7fd022e36d50>{number = 2, name = (null)}
2015-07-28 20:26:28.463 test[18622:4443535] 第0次 - <NSThread: 0x7fd022f237f0>{number = 4, name = (null)}
2015-07-28 20:26:28.463 test[18622:4443533] 第1次 - <NSThread: 0x7fd022d372b0>{number = 3, name = (null)}
2015-07-28 20:26:28.463 test[18622:4443534] 第3次 - <NSThread: 0x7fd022c3ac10>{number = 5, name = (null)}
2015-07-28 20:26:28.463 test[18622:4443536] 第4次 - <NSThread: 0x7fd022e36d50>{number = 2, name = (null)}
OK, 這時應(yīng)該發(fā)問了脚囊,大家將NSOperationQueue
與GCD
的隊列 相比較就會發(fā)現(xiàn),這里沒有串行隊列桐磁,那如果我想要10個任務(wù)在其他線程串行的執(zhí)行怎么辦悔耘?
這就是蘋果封裝的妙處,你不用管串行我擂、并行衬以、同步、異步這些名詞扶踊。NSOperationQueue
有一個參數(shù)maxConcurrentOperationCount
最大并發(fā)數(shù)泄鹏,用來設(shè)置最多可以讓多少個任務(wù)同時執(zhí)行郎任。當(dāng)你把它設(shè)置為 1 的時候秧耗,他不就是串行了嘛!
NSOperationQueue
還有一個添加任務(wù)的方法舶治,- (void)addOperationWithBlock:(void (^)(void))block;
分井,這是不是和GCD
差不多车猬?這樣就可以添加一個任務(wù)到隊列中了,十分方便尺锚。
NSOperation
有一個非常實用的功能珠闰,那就是添加依賴。比如有 3 個任務(wù):A: 從服務(wù)器上下載一張圖片瘫辩,B:給這張圖片加個水印伏嗜,C:把圖片返回給服務(wù)器。這時就可以用到依賴了:
//1.任務(wù)一:下載圖片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載圖片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
}];
//2.任務(wù)二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任務(wù)三:上傳圖片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上傳圖片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.設(shè)置依賴
[operation1 addDependency:operation3]; //任務(wù)二依賴任務(wù)一
[operation3 addDependency:operation2]; //任務(wù)三依賴任務(wù)二
//5.創(chuàng)建隊列并加入任務(wù)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
打印結(jié)果
打水印 - <NSThread: 0x600000276900>{number = 3, name = (null)}
上傳圖片 - <NSThread: 0x604000462400>{number = 4, name = (null)}
下載圖片 - <NSThread: 0x600000276900>{number = 3, name = (null)}
- 注意:不能添加相互依賴伐厌,會死鎖承绸,比如 A依賴B,B依賴A挣轨。
- 可以使用
removeDependency
來解除依賴關(guān)系军熏。 - 可以在不同的隊列之間依賴,反正就是這個依賴是添加到任務(wù)身上的卷扮,和隊列沒關(guān)系荡澎。