學習多線程,轉載兩篇大神的帖子,留著以后回顧!
第一篇:關于iOS多線程,你看我就夠了
第二篇:GCD使用經驗與技巧淺談
都很不錯,還有很多關聯文章,慢慢學!!!多看原帖!!!
第三篇:IOS GCD開發學習中
在這篇文章中,整理一下 iOS 開發中幾種多線程方案,以及其使用方法和注意事項。當然也會給出幾種多線程的案例,在實際使用中感受它們的區別。還有一點需要說明的是,這篇文章將會使用 Swift 和 Objective-c 兩種語言講解!
概述
這篇文章中,我不會說多線程是什么、線程和進程的區別、多線程有什么用,當然我也不會說什么是串行、什么是并行等問題,這些我們應該都知道的。
在 iOS 中其實目前有 4 套多線程方案,他們分別是:
- Pthreads
- NSThread
- GCD (Grand Central Dispatch )
- NSOperation & NSOperationQueue
所以接下來,我會一一講解這些方案的使用方法和一些案例。在將這些內容的時候,我也會順帶說一些多線程周邊產品。比如: 線程同步、 延時執行、 單例模式 等等。
Pthreads
其實這個方案不用說的,只是拿來充個數,為了讓大家了解一下就好了。百度百科里是這么說的:
POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標準。該標準定義了創建和操縱線程的一整套API。在類Unix操作系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為操作系統的線程。
簡單地說,這是一套在很多操作系統上都通用的多線程API,所以移植性很強(然并卵),當然在 iOS 中也是可以的。不過這是基于 c語言 的框架,使用起來這酸爽!感受一下:
define NSEC_PER_SEC 1000000000ull
define USEC_PER_SEC 1000000ull
define NSEC_PER_USEC 1000ull
關鍵詞解釋:
NSEC:納秒。
USEC:微妙。
SEC:秒
PER:每
所以:
NSEC_PER_SEC,每秒有多少納秒。
USEC_PER_SEC,每秒有多少毫秒。(注意是指在納秒的基礎上)
NSEC_PER_USEC,每毫秒有多少納秒。
所以,延時1秒可以寫成如下幾種:
dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);
最后一個“USEC_PER_SEC * NSEC_PER_USEC”,翻譯過來就是“每秒的毫秒數乘以每毫秒的納秒數”,也就是“每秒的納秒數”,所以,延時500毫秒之類的,也就不難了吧~
OBJECTIVE-C
當然第一步要包含頭文件
#import <pthread.h>
然后創建線程,并執行任務
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
pthread_t thread; //創建一個線程并自動執行 pthread_create(&thread, NULL, start, NULL);
}
void *start(void *data) {
NSLog(@"%@", [NSThread currentThread]); return NULL;
}
打印輸出:
2015-07-27 23:57:21.689 testThread[10616:2644653] <NSThread: 0x7fbb48d33690>{number = 2, name = (null)}
看代碼就會發現他需要 c語言函數,這是比較蛋疼的,更蛋疼的是你需要手動處理線程的各個狀態的轉換即管理生命周期,比如,這段代碼雖然創建了一個線程,但并沒有銷毀。
.
SWIFT
很遺憾,在我目前的swift1.2
中無法執行這套方法,原因是這個函數需要傳入一個函數指針CFunctionPointer<T>
類型,但是目前 swift 無法將方法轉換成此類型。聽說swift 2.0
引入一個新特性@convention(c)
, 可以完成 Swift 方法轉換成 c 語言指針的。在這里可以看到
那么,Pthreads
方案的多線程我就介紹這么多,畢竟做 iOS 開發幾乎不可能用到。但是如果你感興趣的話,或者說想要自己實現一套多線程方案,從底層開始定制,那么可以去搜一下相關資料。
NSThread
這套方案是經過蘋果封裝后的,并且完全面向對象的。所以你可以直接操控線程對象,非常直觀和方便。但是,它的生命周期還是需要我們手動管理,所以這套方案也是偶爾用用,比如 [NSThread currentThread],它可以獲取當前線程類,你就可以知道當前線程的各種屬性,用于調試十分方便。下面來看看它的一些用法。
創建并啟動
- 先創建線程類,再啟動
OBJECTIVE-C
// 創建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
// 啟動
[thread start];
SWIFT
//創建
let thread = NSThread(target: self, selector: "run:", object: nil)
//啟動
thread.start()
- 創建并自動啟動
OBJECTIVE-C
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
SWIFT
NSThread.detachNewThreadSelector("run:", toTarget: self, withObject: nil)
- 使用 NSObject 的方法創建并自動啟動
OBJECTIVE-C
[self performSelectorInBackground:@selector(run:) withObject:nil];
SWIFT
很遺憾 too! 蘋果認為performSelector:
不安全,所以在 Swift 去掉了這個方法。
Note: The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe.
其他方法
除了創建啟動外,NSThread 還以很多方法,下面我列舉一些常見的方法,當然我列舉的并不完整,更多方法大家可以去類的定義里去看。
OBJECTIVE-C
//取消線程
- (void)cancel;
//啟動線程
- (void)start;
//判斷某個線程的狀態的屬性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
//設置和獲取線程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
//獲取當前線程信息
+ (NSThread *)currentThread;
//獲取主線程信息+ (NSThread *)mainThread;
//使當前線程暫停一段時間,或者暫停到某個時刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;
SWIFT
Swift的方法名字和OC的方法名都一樣,我就不浪費空間列舉出來了。
其實,NSThread 用起來也挺簡單的,因為它就那幾種方法。同時,我們也只有在一些非常簡單的場景才會用 NSThread, 畢竟它還不夠智能,不能優雅地處理多線程中的其他高級概念。所以接下來要說的內容才是重點。
GCD
Grand Central Dispatch,聽名字就霸氣。它是蘋果為多核的并行運算提出的解決方案,所以會自動合理地利用更多的CPU內核(比如雙核、四核),最重要的是它會自動管理線程的生命周期(創建線程、調度任務、銷毀線程),完全不需要我們管理,我們只需要告訴干什么就行。同時它使用的也是 c語言,不過由于使用了 Block(Swift里叫做閉包),使得使用起來更加方便,而且靈活。
任務和隊列
在 GCD 中,加入了兩個非常重要的概念: 任務 和 隊列。
任務:即操作,你想要干什么,說白了就是一段代碼,在 GCD 中就是一個 Block,所以添加任務十分方便。任務有兩種執行方式: 同步執行 和 異步執行,他們之間的區別是:會不會阻塞當前線程,直到** Block** 中的任務執行完畢!
。
同步執行:會阻塞當前線程并等待 Block 中的任務執行完畢,然后當前線程才會繼續往下運行。
異步執行:當前線程會直接往下執行,它不會阻塞當前線程.隊列:用于存放任務。一共有兩種隊列, 串行隊列 和 并行隊列。
串行隊列 中的任務會根據隊列的定義 FIFO 的執行,一個接一個的先進先出的進行執行。放到串行隊列的任務,GCD 會 FIFO(先進先出) 地取出來一個,執行一個,然后取下一個,這樣一個一個的執行。
并行隊列 放到并行隊列的任務,GCD 也會 FIFO的取出來,但不同的是,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快,忽略不計,看起來,所有的任務都是一起執行的。不過需要注意,GCD 會根據系統資源控制并行的數量,所以如果任務很多,它并不會讓所有任務同時執行。
雖然很繞,但請看下表:
| 同步執行 | 異步執行
----|------------|-----
串行隊列 | 當前線程,一個一個執行 | 其他線程,一個一個執行
并行隊列 | 當前線程,一個一個執行 | 開很多線程,一起執行
創建隊列
- 主隊列:這是一個特殊的 串行隊列。什么是主隊列,大家都知道吧,它用于刷新 UI,任何需要刷新 UI 的工作都要在主隊列執行,所以一般耗時的任務都要放到別的線程執行。
//OBJECTIVE-C
dispatch_queue_t queue = ispatch_get_main_queue();
//SWIFT
let queue = ispatch_get_main_queue()
- 自己創建的隊列:自己可以創建 串行隊列, 也可以創建 并行隊列。其中第一個參數是標識符,用于 DEBUG 的時候標識唯一的隊列,可以為空。第二個參數用來表示創建的隊列是串行的還是并行的,傳入 DISPATCH_QUEUE_SERIAL 或 NULL 表示創建串行隊列。傳入 DISPATCH_QUEUE_CONCURRENT 表示創建并行隊列。大家可以看xcode的文檔查看參數意義。
//OBJECTIVE-C
//串行隊列
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);
//SWIFT
//串行隊列
let queue = dispatch_queue_create("tk.bourne.testQueue", nil);
let queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL)
//并行隊列
let queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT)
- 全局并行隊列:這應該是唯一一個并行隊列, 只要是并行任務一般都加入到這個隊列。這是系統提供的一個并發隊列。
//OBJECTIVE-C
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//SWIFT
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
創建任務
同步任務: 改:會阻塞當前線程 (SYNC)
OBJECTIVE-C
dispatch_sync(<#queue#>, ^{ //code here NSLog(@"%@", [NSThread currentThread]); });
SWIFT
dispatch_sync(<#queue#>, { () -> Void in //code here println(NSThread.currentThread()) })
異步任務:不會阻塞當前線程 (ASYNC)
OBJECTIVE-C
dispatch_async(<#queue#>, ^{ //code here NSLog(@"%@", [NSThread currentThread]); });
SWIFT
dispatch_async(<#queue#>, { () -> Void in //code here println(NSThread.currentThread()) })
為了更好的理解同步和異步,和各種隊列的使用,下面看兩個示例:
示例一:以下代碼在主線程調用,結果是什么?
NSLog("之前 - %@", NSThread.currentThread())
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
NSLog("sync - %@", NSThread.currentThread())
})
NSLog("之后 - %@", NSThread.currentThread())
答案:只會打印第一句:之前 - <NSThread: 0x7fb3a9e16470>{number = 1, name = main}
,然后主線程就卡死了,你可以在界面上放一個按鈕,你就會發現點不了了。解釋:同步任務會阻塞當前線程,然后把 Block 中的任務放到指定的隊列中執行,只有等到 Block 中的任務完成后才會讓當前線程繼續往下運行。那么這里的步驟就是:打印完第一句后,dispatch_sync
立即阻塞當前的主線程,然后把 Block 中的任務放到main_queue
中,可是main_queue
中的任務會被取出來放到主線程中執行,但主線程這個時候已經被阻塞了,所以 Block 中的任務就不能完成,它不完成,dispatch_sync
就會一直阻塞主線程,這就是死鎖現象。導致主線程一直卡死。
示例二:以下代碼會產生什么結果?
let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)
NSLog("之前 - %@", NSThread.currentThread())
dispatch_async(queue, { () -> Void in
NSLog("sync之前 - %@", NSThread.currentThread())
dispatch_sync(queue, { () -> Void in
NSLog("sync - %@", NSThread.currentThread())
})
NSLog("sync之后 - %@", NSThread.currentThread())
})
NSLog("之后 - %@", NSThread.currentThread())
答案:
2015-07-30 02:06:51.058 test[33329:8793087] 之前 - <NSThread: 0x7fe32050dbb0>{number = 1, name = main}
2015-07-30 02:06:51.059 test[33329:8793356] sync之前 - <NSThread: 0x7fe32062e9f0>{number = 2, name = (null)}
2015-07-30 02:06:51.059 test[33329:8793087] 之后 - <NSThread: 0x7fe32050dbb0>{number = 1, name = main}
很明顯sync - %@
和 sync
之后- %@
沒有被打印出來!這是為什么呢?我們再來分析一下:
分析:我們按執行順序一步步來哦:
- 使用
DISPATCH_QUEUE_SERIAL
這個參數,創建了一個 串行隊列。 - 打印出之前
- %@
這句。 -
dispatch_async
異步執行,所以當前線程不會被阻塞,于是有了兩條線程,一條當前線程繼續往下打印出之后- %@
這句, 另一臺執行 Block 中的內容打印sync
之前- %@
這句。因為這兩條是并行的,所以打印的先后順序無所謂。 - 注意,高潮來了。現在的情況和上一個例子一樣了。
dispatch_sync
同步執行,于是它所在的線程會被阻塞,一直等到sync
里的任務執行完才會繼續往下。于是sync
就高興的把自己Block
中的任務放到queue
中,可誰想queue
是一個串行隊列,一次執行一個任務,所以sync
的Block
必須等到前一個任務執行完畢,可萬萬沒想到的是queue
正在執行的任務就是被sync
阻塞了的那個。于是又發生了死鎖。所以sync
所在的線程被卡死了。剩下的兩句代碼自然不會打印。
隊列組
隊列組可以將很多隊列添加到一個組里,這樣做的好處是,當這個組里所有的任務都執行完了,隊列組會通過一個方法通知我們。下面是使用方法,這是一個很實用的功能
OBJECTIVE-C
//1.創建隊列組
dispatch_group_t group = dispatch_group_create();
//2.創建隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.多次使用隊列組的方法執行任務, 只有異步方法
//3.1.執行3次循環
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
//3.2.主隊列執行8次循環
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 8; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});
//3.3.執行5次循環
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]);
});
SWIFT
//1.創建隊列組
let group = dispatch_group_create()
//2.創建隊列let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//3.多次使用隊列組的方法執行任務, 只有異步方法
//3.1.執行3次循環dispatch_group_async(group, queue) { () -> Void in
for _ in 0..<3 {
NSLog("group-01 - %@", NSThread.currentThread())
}
}
//3.2.主隊列執行8次循環
dispatch_group_async(group, dispatch_get_main_queue()) { () -> Void in
for _ in 0..<8 {
NSLog("group-02 - %@", NSThread.currentThread())
}
}
//3.3.執行5次循環
dispatch_group_async(group, queue) { () -> Void in
for _ in 0..<5 {
NSLog("group-03 - %@", NSThread.currentThread())
}
}
//4.都完成后會自動通知
dispatch_group_notify(group, dispatch_get_main_queue()) { () -> Void in
NSLog("完成 - %@", NSThread.currentThread())
}
打印結果
2015-07-28 03:40:34.277 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}
2015-07-28 03:40:34.277 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.277 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.277 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.277 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319271] group-03 - <NSThread: 0x7f9772536f00>{number = 3, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.278 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}
2015-07-28 03:40:34.278 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.278 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number = 2, name = (null)}
2015-07-28 03:40:34.279 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.279 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
2015-07-28 03:40:34.279 test[12540:3319146] 完成 - <NSThread: 0x7f977240ba60>{number = 1, name = main}
這些就是 GCD 的基本功能,但是它的能力遠不止這些,等講完 NSOperation 后,我們再來看看它的一些其他方面用途。而且,只要你想象力夠豐富,你可以組合出更好的用法。
關于GCD,還有兩個需要說的:
-
func dispatch_barrier_async(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
這個方法重點是你傳入的 queue,當你傳入的 queue 是通過DISPATCH_QUEUE_CONCURRENT
參數自己創建的 queue 時,這個方法會阻塞這個 queue(注意是阻塞 queue ,而不是阻塞當前線程),一直等到這個 queue 中排在它前面的任務都執行完成后才會開始執行自己,自己執行完畢后,再會取消阻塞,使這個 queue 中排在它后面的任務繼續執行。如果你傳入的是其他的 queue, 那么它就和dispatch_async
一樣了。 -
func dispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
這個方法的使用和上一個一樣,傳入 自定義的并發隊列(DISPATCH_QUEUE_CONCURRENT)
,它和上一個方法一樣的阻塞 queue,不同的是 這個方法還會 阻塞當前線程。如果你傳入的是其他的 queue, 那么它就和dispatch_sync
一樣了。
NSOperation和NSOperationQueue
NSOperation 是蘋果公司對 GCD 的封裝,完全面向對象,所以使用起來更好理解。 大家可以看到 NSOperation
和 NSOperationQueue
分別對應 GCD 的 任務 和 隊列 。操作步驟也很好理解:
將要執行的任務封裝到一個 NSOperation
對象中。
將此任務添加到一個 NSOperationQueue
對象中。
然后系統就會自動在執行任務。至于同步還是異步、串行還是并行請繼續往下看:
添加任務
值得說明的是,NSOperation
只是一個抽象類,所以不能封裝任務。但它有 2 個子類用于封裝任務。分別是:NSInvocationOperation
和NSBlockOperation
。創建一個 Operation
后,需要調用start
方法來啟動任務,它會 默認在當前隊列同步執行。當然你也可以在中途取消一個任務,只需要調用其cancel
方法即可。
- NSInvocationOperation : 需要傳入一個方法名。
OBJECTIVE-C
//1.創建NSInvocationOperation對象
NSInvocationOperation *operation =[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
//2.開始執行
[operation start];
SWIFT
在 Swift 構建的和諧社會里,是容不下NSInvocationOperation
這種不是類型安全的敗類的。蘋果如是說。這里有相關解釋
- NSBlockOperation
OBJECTIVE-C
//1.創建NSBlockOperation對象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//2.開始任務
[operation start];
SWIFT
//1.創建NSBlockOperation對象
let operation = NSBlockOperation { () -> Void in
println(NSThread.currentThread())
}
//2.開始任務
operation.start()
之前說過這樣的任務,默認會在當前線程執行。但是NSBlockOperation
還有一個方法:addExecutionBlock:
,通過這個方法可以給 Operation
添加多個執行 Block
。這樣 Operation 中的任務 會并發執行,它會 在主線程和其它的多個線程 執行這些任務,注意下面的打印結果:
OBJECTIVE-C
//1.創建NSBlockOperation對象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//添加多個Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//2.開始任務
[operation start];
SWIFT
//1.創建NSBlockOperation對象
let operation = NSBlockOperation { () -> Void in
NSLog("%@", NSThread.currentThread())
}
//2.添加多個Block
for i in 0..<5 {
operation.addExecutionBlock { () -> Void in
NSLog("第%ld次 - %@", i, NSThread.currentThread())
}
}
//2.開始任務
operation.start()
打印輸出
2015-07-28 17:50:16.585 test[17527:4095467] 第2次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}
2015-07-28 17:50:16.585 test[17527:4095666] 第1次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}
2015-07-28 17:50:16.585 test[17527:4095665] <NSThread: 0x7ff5c961b610>{number = 3, name = (null)}
2015-07-28 17:50:16.585 test[17527:4095662] 第0次 - <NSThread: 0x7ff5c948d310>{number = 2, name = (null)}
2015-07-28 17:50:16.586 test[17527:4095666] 第3次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}
2015-07-28 17:50:16.586 test[17527:4095467] 第4次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}
NOTE:addExecutionBlock
方法必須在start()
方法之前執行,否則就會報錯:
*** -[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished'
NOTE:大家可能發現了一個問題,為什么我在 Swift 里打印輸出使用NSLog()
而不是println()
呢?原因是使用print() / println()
輸出的話,它會簡單地使用 流(stream) 的概念,學過 C++ 的都知道。它會把需要輸出的每個字符一個一個的輸出到控制臺。普通使用并沒有問題,可是當多線程同步輸出的時候問題就來了,由于很多println()
同時打印,就會導致控制臺上的字符混亂的堆在一起,而NSLog()
就沒有這個問題。到底是什么樣子的呢?你可以把上面NSLog()
改為println()
,然后一試便知。 更多NSLog()
與 println()
的區別看這里
- 自定義Operation
除了上面的兩種 Operation
以外,我們還可以自定義 Operation
。自定義 Operation
需要繼承 NSOperation
類,并實現其 main()
方法,因為在調用 start()
方法的時候,內部會調用main()
方法完成相關邏輯。所以如果以上的兩個類無法滿足你的欲望的時候,你就需要自定義了。你想要實現什么功能都可以寫在里面。除此之外,你還需要實現cancel()
在內的各種方法。所以這個功能提供給高級玩家,我在這里就不說了,等我需要用到時在研究它,到時候可能會再做更新。
創建隊列
看過上面的內容就知道,我們可以調用一個NSOperation
對象的start()
方法來啟動這個任務,但是這樣做他們默認是 同步執行 的。就算是addExecutionBlock
方法,也會在 當前線程和其他線程 中執行,也就是說還是會占用當前線程。這是就要用到隊列NSOperationQueue
了。而且,按類型來說的話一共有兩種類型:主隊列、其他隊列。只要添加到隊列,會自動調用任務的 start()
方法
- 主隊列
細心的同學就會發現,每套多線程方案都會有一個主線程(當然啦,說的是iOS中,像 pthread
這種多系統的方案并沒有,因為 UI線程 理論需要每種操作系統自己定制)。這是一個特殊的線程,必須串行。所以添加到主隊列的任務都會一個接一個地排著隊在主線程處理。
//OBJECTIVE-C
NSOperationQueue *queue = [NSOperationQueue mainQueue];
//SWIFT
let queue = NSOperationQueue.mainQueue()
- 其他隊列
因為主隊列比較特殊,所以會單獨有一個類方法來獲得主隊列。那么通過初始化產生的隊列就是其他隊列了,因為只有這兩種隊列,除了主隊列,其他隊列就不需要名字了。
注意:其他隊列的任務會在其他線程并行執行。
OBJECTIVE-C
//1.創建一個其他隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//2.創建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.隊列添加任務
[queue addOperation:operation];
SWIFT
//1.創建其他隊列
let queue = NSOperationQueue()
//2.創建NSBlockOperation對象
let operation = NSBlockOperation { () -> Void in
NSLog("%@", NSThread.currentThread())
}
//3.添加多個Block
for i in 0..<5 {
operation.addExecutionBlock { () -> Void in
NSLog("第%ld次 - %@", i, NSThread.currentThread())
}
}
//4.隊列添加任務
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, 這時應該發問了,大家將NSOperationQueue
與 GCD的隊列 相比較就會發現,這里沒有串行隊列,那如果我想要10個任務在其他線程串行的執行怎么辦?
這就是蘋果封裝的妙處,你不用管串行、并行、同步、異步這些名詞。NSOperationQueue
有一個參數 maxConcurrentOperationCount
最大并發數,用來設置最多可以讓多少個任務同時執行。當你把它設置為 1 的時候,他不就是串行了嘛!
NSOperationQueue
還有一個添加任務的方法,- (void)addOperationWithBlock:(void (^)(void))block;
,這是不是和 GCD 差不多?這樣就可以添加一個任務到隊列中了,十分方便。
NSOperation 有一個非常實用的功能,那就是添加依賴。比如有 3 個任務:A: 從服務器上下載一張圖片,B:給這張圖片加個水印,C:把圖片返回給服務器。這時就可以用到依賴了:
OBJECTIVE-C
//1.任務一:下載圖片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下載圖片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任務二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任務三:上傳圖片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上傳圖片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.設置依賴
[operation2 addDependency:operation1]; //任務二依賴任務一
[operation3 addDependency:operation2]; //任務三依賴任務二
//5.創建隊列并加入任務
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
SWIFT
//1.任務一:下載圖片
let operation1 = NSBlockOperation { () -> Void in
NSLog("下載圖片 - %@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//2.任務二:打水印
let operation2 = NSBlockOperation { () -> Void in
NSLog("打水印 - %@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//3.任務三:上傳圖片
let operation3 = NSBlockOperation { () -> Void in
NSLog("上傳圖片 - %@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//4.設置依賴
operation2.addDependency(operation1) //任務二依賴任務一
operation3.addDependency(operation2) //任務三依賴任務二
//5.創建隊列并加入任務
let queue = NSOperationQueue()
queue.addOperations([operation3, operation2, operation1], waitUntilFinished: false)
打印結果
2015-07-28 21:24:28.622 test[19392:4637517] 下載圖片 - <NSThread: 0x7fc10ad4d970>{number = 2, name = (null)}
2015-07-28 21:24:29.622 test[19392:4637515] 打水印 - <NSThread: 0x7fc10af20ef0>{number = 3, name = (null)}
2015-07-28 21:24:30.627 test[19392:4637515] 上傳圖片 - <NSThread: 0x7fc10af20ef0>{number = 3, name = (null)}
注意:
- 不能添加相互依賴,會死鎖,比如 A依賴B,B依賴A。
- 可以使用 removeDependency 來解除依賴關系。
- 可以在不同的隊列之間依賴,反正就是這個依賴是添加到任務身上的,和隊列沒關系。
其他方法
以上就是一些主要方法, 下面還有一些常用方法需要大家注意:
- NSOperation
BOOL executing; //判斷任務是否正在執行
BOOL finished; //判斷任務是否完成
-void (^completionBlock)(void); //用來設置完成后需要執行的操作
-(void)cancel; //取消任務
-(void)waitUntilFinished; //阻塞當前線程直到此任務執行完畢
- NSOperationQueue
NSUInteger operationCount; //獲取隊列的任務數
-(void)cancelAllOperations; //取消隊列中所有的任務
-(void)waitUntilAllOperationsAreFinished; //阻塞當前線程直到此隊列中的所有任務執行完畢
[queue setSuspended:YES]; // 暫停queue
[queue setSuspended:NO]; // 繼續queue
好啦,到這里差不多就講完了。當然,我講的并不完整,可能有一些知識我并沒有講到,但作為常用方法,這些已經足夠了。不過我在這里只是告訴你了一些方法的功能,只是怎么把他們用到合適的地方,就需要多多實踐了。下面我會說一些關于多線程的案例,是大家更加什么地了解。
其他用法
在這部分,我會說一些和多線程知識相關的案例,可能有些很簡單,大家早都知道的,不過因為這篇文章講的是多線程嘛,所以應該盡可能的全面嘛。還有就是,我會盡可能的使用多種方法實現,讓大家看看其中的區別。
線程同步
所謂線程同步就是為了防止多個線程搶奪同一個資源造成的數據安全問題,所采取的一種措施。當然也有很多實現方法,請往下看:
- 互斥鎖 :給需要同步的代碼塊加一個互斥鎖,就可以保證每次只有一個線程訪問此代碼塊。
OBJECTIVE-C
@synchronized(self) { //需要執行的代碼塊}
SWIFT
objc_sync_enter(self)//需要執行的代碼塊objc_sync_exit(self)
-
同步執行 :我們可以使用多線程的知識,把多個線程都要執行此段代碼添加到同一個串行隊列,這樣就實現了線程同步的概念。當然這里可以使用GCD
和NSOperation
兩種方案,我都寫出來。
OBJECTIVE-C
//GCD
//需要一個全局變量queue,要讓所有線程的這個操作都加到一個queue中
dispatch_sync(queue, ^{
NSInteger ticket = lastTicket;
[NSThread sleepForTimeInterval:0.1];
NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
ticket -= 1;
lastTicket = ticket;
});
//NSOperation & NSOperationQueue
//重點:1. 全局的 NSOperationQueue, 所有的操作添加到同一個queue中
// 2. 設置 queue 的 maxConcurrentOperationCount 為 1
// 3. 如果后續操作需要Block中的結果,就需要調用每個操作的waitUntilFinished,阻塞當前線程,一直等到當前操作完成,才允許執行后面的。waitUntilFinished 要在添加到隊列之后!
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSInteger ticket = lastTicket;
[NSThread sleepForTimeInterval:1];
NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
ticket -= 1;
lastTicket = ticket;
}];
[queue addOperation:operation];
[operation waitUntilFinished];
//后續要做的事
SWIFT
這里的 swift 代碼,我就不寫了,因為每句都一樣,只是語法不同而已,照著 OC 的代碼就能寫出 Swift 的。這篇文章已經老長老長了,我就不浪費篇幅了,又不是高中寫作文。
延遲執行
所謂延遲執行就是延時一段時間再執行某段代碼。下面說一些常用方法。
- perform
OBJECTIVE-C
// 3秒后自動調用self的run:方法,并且傳遞參數:@"abc" [self performSelector:@selector(run:) withObject:@"abc" afterDelay:3];
SWIFT
之前就已經說過,Swift 里去掉了這個方法。
- GCD
可以使用 GCD 中的dispatch_after
方法,OC 和 Swift 都可以使用,這里只寫 OC 的,Swift 的是一樣的。
OBJECTIVE-C
// 創建隊列
// 創建隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 設置延時,單位秒
double delay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, ^{
// 3秒后需要執行的任務
});
- NSTimer
NSTimer 是iOS中的一個計時器類,除了延遲執行還有很多用法,不過這里直說延遲執行的用法。同樣只寫 OC 版的,Swift 也是相同的。
OBJECTIVE-C
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(run:) userInfo:@"abc" repeats:NO];
單例模式
至于什么是單例模式,我也不多說,我只說說一般怎么實現。在 Objective-C 中,實現單例的方法已經很具體了,雖然有別的方法,但是一般都是用一個標準的方法了,下面來看看。
OBJECTIVE-C
@interface Tool : NSObject <NSCopying>
+ (instancetype)sharedTool;
@end
@implementation Tool
static id _instance;
+ (instancetype)sharedTool {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[Tool alloc] init];
});
return _instance;
}
@end
這里之所以將單例模式,是因為其中用到了 GCD 的 dispatch_once
方法。下面看 Swift 中的單例模式,在Swift中單例模式非常簡單!想知道怎么從 OC 那么復雜的方法變成下面的寫法的,請看這里
SWIFT
class Tool: NSObject {
static let sharedTool = Tool()
// 私有化構造方法,阻止其他對象使用這個類的默認的'()'構造方法
private override init() {}
}
從其他線程回到主線程的方法
我們都知道在其他線程操作完成后必須到主線程更新UI。所以,介紹完所有的多線程方案后,我們來看看有哪些方法可以回到主線程。
- NSThread
//Objective-C
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
//Swift
//swift 取消了 performSelector 方法。
- GCD
//Objective-C
dispatch_async(dispatch_get_main_queue(), ^{
});
//Swift
dispatch_async(dispatch_get_main_queue(), { () -> Void in
})
- NSOperationQueue
//Objective-C
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];
//Swift
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
}
其他
dispatch_suspend != 立即停止隊列的運行
dispatch_suspend,dispatch_resume提供了“掛起、恢復”隊列的功能,簡單來說,就是可以暫停、恢復隊列上的任務。但是這里的“掛起”,并不能保證可以立即停止隊列上正在運行的block,看如下例子:
dispatch_queue_t queue = dispatch_queue_create(“me.tutuge.test.gcd”, DISPATCH_QUEUE_SERIAL);
//提交第一個block,延時5秒打印。
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@”After 5 seconds…”);
});
//提交第二個block,也是延時5秒打印
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@”After 5 seconds again…”);
});
//延時一秒
NSLog(@”sleep 1 second…”);
[NSThread sleepForTimeInterval:1];
//掛起隊列
NSLog(@”suspend…”);
dispatch_suspend(queue);
//延時10秒
NSLog(@”sleep 10 second…”);
[NSThread sleepForTimeInterval:10];
//恢復隊列
NSLog(@”resume…”);
dispatch_resume(queue);
運行結果如下:
2015-04-01 00:32:09.903 GCDTest[47201:1883834] sleep 1 second…
2015-04-01 00:32:10.910 GCDTest[47201:1883834] suspend…
2015-04-01 00:32:10.910 GCDTest[47201:1883834] sleep 10 second…
2015-04-01 00:32:14.908 GCDTest[47201:1883856] After 5 seconds…
2015-04-01 00:32:20.911 GCDTest[47201:1883834] resume…
2015-04-01 00:32:25.912 GCDTest[47201:1883856] After 5 seconds again…
可知,在dispatch_suspend掛起隊列后,第一個block還是在運行,并且正常輸出。
結合文檔,我們可以得知,dispatch_suspend并不會立即暫停正在運行的block,而是在當前block執行完成后,暫停后續的block執行。
所以下次想暫停正在隊列上運行的block時,還是不要用dispatch_suspend了吧~
“同步”的dispatch_apply
dispatch_apply的作用是在一個隊列(串行或并行)上“運行”多次block,其實就是簡化了用循環去向隊列依次添加block任務。但是我個人覺得這個函數就是個“坑”,先看看如下代碼運行結果:
//創建異步串行隊列
dispatch_queue_t queue = dispatch_queue_create(“me.tutuge.test.gcd”, DISPATCH_QUEUE_SERIAL);
//運行block3次
dispatch_apply(3, queue, ^(size_t i) {
NSLog(@”apply loop: %zu”, i);
});
//打印信息
NSLog(@”After apply”);
運行的結果是:
2015-04-01 00:55:40.854 GCDTest[47402:1893289] apply loop: 0
2015-04-01 00:55:40.856 GCDTest[47402:1893289] apply loop: 1
2015-04-01 00:55:40.856 GCDTest[47402:1893289] apply loop: 2
2015-04-01 00:55:40.856 GCDTest[47402:1893289] After apply
看,明明是提交到異步的隊列去運行,但是“After apply”居然在apply后打印,也就是說,dispatch_apply將外面的線程(main線程)“阻塞”了!
查看官方文檔,dispatch_apply確實會“等待”其所有的循環運行完畢才往下執行=。=,看來要小心使用了。
避免死鎖!
dispatch_sync導致的死鎖
涉及到多線程的時候,不可避免的就會有“死鎖”這個問題,在使用GCD時,往往一不小心,就可能造成死鎖,看看下面的“死鎖”例子:
//在main線程使用“同步”方法提交Block,必定會死鎖。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@”I am block…”);
});
你可能會說,這么低級的錯誤,我怎么會犯,那么,看看下面的:
(void)updateUI1 {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@”Update ui 1”);
//死鎖!
[self updateUI2];
});
}
(void)updateUI2 {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@”Update ui 2”);
});
}
在你不注意的時候,嵌套調用可能就會造成死鎖!所以為了“世界和平”=。=,我們還是少用dispatch_sync吧。
dispatch_apply導致的死鎖!
dispatch_apply導致的死鎖?。。。是的,前一節講到,dispatch_apply會等循環執行完成,這不就差不多是阻塞了嗎。看如下例子:
dispatch_queue_t queue = dispatch_queue_create(“me.tutuge.test.gcd”, DISPATCH_QUEUE_SERIAL);
dispatch_apply(3, queue, ^(size_t i) {
NSLog(@”apply loop: %zu”, i);
//再來一個dispatch_apply!死鎖!
dispatch_apply(3, queue, ^(size_t j) {
NSLog(@"apply loop inside %zu", j);
});
});
這段代碼只會輸出“apply loop: 1”。。。就沒有然后了=。=
所以,一定要避免dispatch_apply的嵌套調用。
靈活使用dispatch_group
很多時候我們需要等待一系列任務(block)執行完成,然后再做一些收尾的工作。如果是有序的任務,可以分步驟完成的,直接使用串行隊列就行。但是如果是一系列并行執行的任務呢?這個時候,就需要dispatch_group幫忙了~總的來說,dispatch_group的使用分如下幾步:
- 創建dispatch_group_t
- 添加任務(block)
- 添加結束任務(如清理操作、通知UI等)
下面著重講講在后面兩步。
1. 添加任務
添加任務可以分為以下兩種情況:
(1)自己創建隊列:使用dispatch_group_async。
(2)無法直接使用隊列變量(如使用AFNetworking添加異步任務):使用dispatch_group_enter,dispatch_group_leave。
自己創建隊列時,當然就用dispatch_group_async函數,簡單有效,簡單例子如下:
//省去創建group、queue代碼。。。
dispatch_group_async(group, queue, ^{
//Do you work…
});
當你無法直接使用隊列變量時,就無法使用dispatch_group_async了,下面以使用AFNetworking時的情況:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
//Enter group
dispatch_group_enter(group);
[manager GET:@”http://www.baidu.com” parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
//Deal with result…
//Leave group
dispatch_group_leave(group);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//Deal with error…
//Leave group
dispatch_group_leave(group);
}];
//More request…
使用dispatch_group_enter,dispatch_group_leave就可以方便的將一系列網絡請求“打包”起來~
2. 添加結束任務
添加結束任務也可以分為兩種情況,如下:
(1)在當前線程阻塞的同步等待:dispatch_group_wait。
(2)添加一個異步執行的任務作為結束任務:dispatch_group_notify
這兩個比較簡單,就不再貼代碼了=。=
使用dispatch_barrier_async,dispatch_barrier_sync的注意事項
dispatch_barrier_async的作用就是向某個隊列插入一個block,當目前正在執行的block運行完成后,阻塞這個block后面添加的block,只運行這個block直到完成,然后再繼續后續的任務,有點“唯我獨尊”的感覺=。=
值得注意的是:
dispatchbarrier(a)sync只在自己創建的并發隊列上有效,在全局(Global)并發隊列、串行隊列上,效果跟dispatch_(a)sync效果一樣。
既然在串行隊列上跟dispatch_(a)sync效果一樣,那就要小心別死鎖!
dispatch_set_context與dispatch_set_finalizer_f的配合使用
dispatch_set_context可以為隊列添加上下文數據,但是因為GCD是C語言接口形式的,所以其context參數類型是“void *”。也就是說,我們創建context時有如下幾種選擇:
- 用C語言的malloc創建context數據。
- 用C++的new創建類對象。
- 用Objective-C的對象,但是要用__bridge等關鍵字轉為Core Foundation對象。
以上所有創建context的方法都有一個必須的要求,就是都要釋放內存!,無論是用free、delete還是CF的CFRelease,我們都要確保在隊列不用的時候,釋放context的內存,否則就會造成內存泄露。
所以,使用dispatch_set_context的時候,最好結合dispatch_set_finalizer_f使用,為隊列設置“析構函數”,在這個函數里面釋放內存,大致如下:
void cleanStaff(void *context) {
//釋放context的內存!
//CFRelease(context);
//free(context);
//delete context;
}
參考
Grand Central Dispatch (GCD) Reference
Concurrency Programming Guide
Using Dispatch Groups to Wait for Multiple Web Services