在這篇文章中,我將為你整理一下 iOS 開發中幾種多執行緒方案,以及其使用方法和注意事項。當然也會給出幾種多執行緒的案例,在實際使用中感受它們的區別。還有一點需要說明的是,這篇文章將會使用 Swift
和 Objective-c
兩種語言講解,雙語幼兒園。OK,let’s begin!
概述
這篇文章中,我不會說多執行緒是什麼、執行緒和程式的區別、多執行緒有什麼用,當然我也不會說什麼是序列、什麼是並行等問題,這些我們應該都知道的。
在 iOS 中其實目前有 4
套多執行緒方案,他們分別是:
- Pthreads
- NSThread
- GCD
- NSOperation & NSOperationQueue
所以接下來,我會一一講解這些方案的使用方法和一些案例。在將這些內容的時候,我也會順帶說一些多執行緒周邊產品。比如: 執行緒同步、 延時執行、 單例模式 等等。
Pthreads
其實這個方案不用說的,只是拿來充個數,為了讓大家瞭解一下就好了。百度百科裡是這麼說的:
POSIX執行緒(POSIX threads),簡稱Pthreads,是執行緒的POSIX標準。該標準定義了建立和操縱執行緒的一整套API。在類Unix作業系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為作業系統的執行緒。
簡單地說,這是一套在很多作業系統上都通用的多執行緒API,所以移植性很強(然並卵),當然在 iOS 中也是可以的。不過這是基於 c語言 的框架,使用起來這酸爽!感受一下:
OBJECTIVE-C
當然第一步要包含標頭檔案
#import <pthread.h>
然後建立執行緒,並執行任務
1 2 3 4 5 6 7 8 9 10 11 |
- (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
12345// 建立NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];// 啟動[thread start];SWIFT
12345//建立let thread = NSThread(target: self, selector: "run:", object: nil)//啟動thread.start() - 建立並自動啟動
OBJECTIVE-C
1[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];SWIFT
1 |
NSThread.detachNewThreadSelector("run:", toTarget: self, withObject: nil) |
使用 NSObject 的方法建立並自動啟動
OBJECTIVE-C
1 |
[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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//取消執行緒 - (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
中,加入了兩個非常重要的概念: 任務 和 佇列。
- 任務:即操作,你想要幹什麼,說白了就是一段程式碼,在 GCD 中就是一個 Block,所以新增任務十分方便。任務有兩種執行方式: 同步執行 和 非同步執行,他們之間的區別是
是否會建立新的執行緒
。同步執行:只要是同步執行的任務,都會在當前執行緒執行,不會另開執行緒。非同步執行:只要是非同步執行的任務,都會另開執行緒,在別的執行緒執行。
更新:
這裡說的並不準確,同步(sync)
和非同步(async)
的主要區別在於會不會阻塞當前執行緒,直到Block
中的任務執行完畢!
如果是同步(sync)
操作,它會阻塞當前執行緒並等待Block
中的任務執行完畢,然後當前執行緒才會繼續往下執行。
如果是非同步(async)
操作,當前執行緒會直接往下執行,它不會阻塞當前執行緒。 - 佇列:用於存放任務。一共有兩種佇列, 序列佇列 和 並行佇列。序列佇列 中的任務會根據佇列的定義 FIFO 的執行,一個接一個的先進先出的進行執行。
更新:放到序列佇列的任務,GCD 會
FIFO(先進先出)
地取出來一個,執行一個,然後取下一個,這樣一個一個的執行。並行佇列 中的任務
根據同步或非同步有不同的執行方式。更新:放到序列佇列的任務,GCD 也會
FIFO
的取出來,但不同的是,它取出來一個就會放到別的執行緒,然後再取出來一個又放到另一個的執行緒。這樣由於取的動作很快,忽略不計,看起來,所有的任務都是一起執行的。不過需要注意,GCD 會根據系統資源控制並行的數量,所以如果任務很多,它並不會讓所有任務同時執行。
雖然很繞,但請看下錶:
同步執行 | 非同步執行 | |
---|---|---|
序列佇列 | 當前執行緒,一個一個執行 | 其他執行緒,一個一個執行 |
並行佇列 | 當前執行緒,一個一個執行 | 開很多執行緒,一起執行 |
建立佇列
- 主佇列:這是一個特殊的
序列佇列
。什麼是主佇列,大家都知道吧,它用於重新整理 UI,任何需要重新整理 UI 的工作都要在主佇列執行,所以一般耗時的任務都要放到別的執行緒執行。
12345//OBJECTIVE-Cdispatch_queue_t queue = ispatch_get_main_queue();//SWIFTlet queue = ispatch_get_main_queue() - 自己建立的佇列:
凡是自己建立的佇列都是其中第一個引數是識別符號,用於 DEBUG 的時候標識唯一的佇列,可以為空。大家可以看xcode的文件檢視引數意義。序列佇列
。
更新:自己可以建立
序列佇列
, 也可以建立並行佇列
。看下面的程式碼(程式碼已更新),它有兩個引數,第一個上面已經說了,第二個才是最重要的。
第二個引數用來表示建立的佇列是序列的還是並行的,傳入DISPATCH_QUEUE_SERIAL
或NULL
表示建立序列佇列。傳入DISPATCH_QUEUE_CONCURRENT
表示建立並行佇列。12345678910111213//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) - 全域性並行佇列:
這應該是唯一一個並行佇列,只要是並行任務一般都加入到這個佇列。這是系統提供的一個併發佇列。12345//OBJECTIVE-Cdispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//SWIFTlet queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
建立任務
- 同步任務:
不會另開執行緒改:會阻塞當前執行緒 (SYNC)
OBJECTIVE-C
1234dispatch_sync(<#queue#>, ^{//code hereNSLog(@"%@", [NSThread currentThread]);});SWIFT
1234dispatch_sync(<#queue#>, { () -> Void in//code hereprintln(NSThread.currentThread())}) - 非同步任務:
會另開執行緒改:不會阻塞當前執行緒 (ASYNC)
OBJECTIVE-C
1234dispatch_async(<#queue#>, ^{//code hereNSLog(@"%@", [NSThread currentThread]);});SWIFT
1234dispatch_async(<#queue#>, { () -> Void in//code hereprintln(NSThread.currentThread())})
更新:
為了更好的理解同步和非同步,和各種佇列的使用,下面看兩個示例:示例一:
以下程式碼在主執行緒呼叫,結果是什麼?
12345 NSLog("之前 - %@", NSThread.currentThread())dispatch_sync(dispatch_get_main_queue(), { () -> Void inNSLog("sync - %@", NSThread.currentThread())})NSLog("之後 - %@", NSThread.currentThread())答案:
只會列印第一句:之前 - <NSThread: 0x7fb3a9e16470>{number = 1, name = main}
,然後主執行緒就卡死了,你可以在介面上放一個按鈕,你就會發現點不了了。
解釋:
同步任務會阻塞當前執行緒,然後把 Block 中的任務放到指定的佇列中執行,只有等到 Block 中的任務完成後才會讓當前執行緒繼續往下執行。
那麼這裡的步驟就是:列印完第一句後,dispatch_sync
立即阻塞當前的主執行緒,然後把 Block 中的任務放到main_queue
中,可以main_queue
中的任務會被取出來放到主執行緒中執行,但主執行緒這個時候已經被阻塞了,所以 Block 中的任務就不能完成,它不完成,dispatch_sync
就會一直阻塞主執行緒,這就是死鎖現象。導致主執行緒一直卡死。示例二:
以下程式碼會產生什麼結果?
12345678910111213 let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)NSLog("之前 - %@", NSThread.currentThread())dispatch_async(queue, { () -> Void inNSLog("sync之前 - %@", NSThread.currentThread())dispatch_sync(queue, { () -> Void inNSLog("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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//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
12345//1.建立NSInvocationOperation物件NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];//2.開始執行[operation start];SWIFT
在 Swift 構建的和諧社會裡,是容不下
NSInvocationOperation
這種不是型別安全的敗類的。蘋果如是說。這裡有相關解釋
- NSBlockOperation
OBJECTIVE-C
1234567//1.建立NSBlockOperation物件NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{NSLog(@"%@", [NSThread currentThread]);}];//2.開始任務[operation start];SWIFT
1234567//1.建立NSBlockOperation物件let operation = NSBlockOperation { () -> Void inprintln(NSThread.currentThread())}//2.開始任務operation.start()之前說過這樣的任務,預設會在當前執行緒執行。但是
NSBlockOperation
還有一個方法:addExecutionBlock:
,通過這個方法可以給 Operation 新增多個執行 Block。這樣 Operation 中的任務 會併發執行,它會 在主執行緒和其它的多個執行緒 執行這些任務,注意下面的列印結果:OBJECTIVE-C
1234567891011121314//1.建立NSBlockOperation物件NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{NSLog(@"%@", [NSThread currentThread]);}];//新增多個Blockfor (NSInteger i = 0; i < 5; i++) {[operation addExecutionBlock:^{NSLog(@"第%ld次:%@", i, [NSThread currentThread]);}];}//2.開始任務[operation start];SWIFT
1234567891011121314//1.建立NSBlockOperation物件let operation = NSBlockOperation { () -> Void inNSLog("%@", NSThread.currentThread())}//2.新增多個Blockfor i in 0..<5 {operation.addExecutionBlock { () -> Void inNSLog("第%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執行緒 理論需要每種作業系統自己定製)。這是一個特殊的執行緒,必須序列。所以新增到主佇列的任務都會一個接一個地排著隊在主執行緒處理。
12345//OBJECTIVE-CNSOperationQueue *queue = [NSOperationQueue mainQueue];//SWIFTlet queue = NSOperationQueue.mainQueue()
- 其他佇列因為主佇列比較特殊,所以會單獨有一個類方法來獲得主佇列。那麼通過初始化產生的佇列就是其他佇列了,因為只有這兩種佇列,除了主佇列,其他佇列就不需要名字了。注意:其他佇列的任務會在其他執行緒並行執行。
OBJECTIVE-C
1234567891011121314151617//1.建立一個其他佇列NSOperationQueue *queue = [[NSOperationQueue alloc] init];//2.建立NSBlockOperation物件NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{NSLog(@"%@", [NSThread currentThread]);}];//3.新增多個Blockfor (NSInteger i = 0; i < 5; i++) {[operation addExecutionBlock:^{NSLog(@"第%ld次:%@", i, [NSThread currentThread]);}];}//4.佇列新增任務[queue addOperation:operation];SWIFT
1234567891011121314151617//1.建立其他佇列let queue = NSOperationQueue()//2.建立NSBlockOperation物件let operation = NSBlockOperation { () -> Void inNSLog("%@", NSThread.currentThread())}//3.新增多個Blockfor i in 0..<5 {operation.addExecutionBlock { () -> Void inNSLog("第%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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//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
123@synchronized(self) {//需要執行的程式碼塊}SWIFT
123objc_sync_enter(self)//需要執行的程式碼塊objc_sync_exit(self) - 同步執行 :我們可以使用多執行緒的知識,把多個執行緒都要執行此段程式碼新增到同一個序列佇列,這樣就實現了執行緒同步的概念。當然這裡可以使用
GCD
和NSOperation
兩種方案,我都寫出來。
OBJECTIVE-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//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
12// 3秒後自動呼叫self的run:方法,並且傳遞引數:@"abc"[self performSelector:@selector(run:) withObject:@"abc" afterDelay:3];SWIFT
1之前就已經說過,Swift 裡去掉了這個方法。 - GCD可以使用 GCD 中的
dispatch_after
方法,OC 和 Swift 都可以使用,這裡只寫 OC 的,Swift 的是一樣的。
OBJECTIVE-C
12345678// 建立佇列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秒後需要執行的任務}); - NSTimerNSTimer 是iOS中的一個計時器類,除了延遲執行還有很多用法,不過這裡直說延遲執行的用法。同樣只寫 OC 版的,Swift 也是相同的。
OBJECTIVE-C
1[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(run:) userInfo:@"abc" repeats:NO];
單例模式
至於什麼是單例模式,我也不多說,我只說說一般怎麼實現。在 Objective-C 中,實現單例的方法已經很具體了,雖然有別的方法,但是一般都是用一個標準的方法了,下面來看看。
OBJECTIVE-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@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
1 2 3 4 5 6 |
class Tool: NSObject { static let sharedTool = Tool() // 私有化構造方法,阻止其他物件使用這個類的預設的'()'構造方法 private override init() {} } |
從其他執行緒回到主執行緒的方法
我們都知道在其他執行緒操作完成後必須到主執行緒更新UI。所以,介紹完所有的多執行緒方案後,我們來看看有哪些方法可以回到主執行緒。
- NSThread
12345//Objective-C[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];//Swift//swift 取消了 performSelector 方法。 - GCD
123456789//Objective-Cdispatch_async(dispatch_get_main_queue(), ^{});//Swiftdispatch_async(dispatch_get_main_queue(), { () -> Void in}) - NSOperationQueue
123456789//Objective-C[[NSOperationQueue mainQueue] addOperationWithBlock:^{}];//SwiftNSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in}
總結
好的吧,總算寫完了,純手敲6k多字,感動死我了。花了兩天,時間跨度有點大,所以可能有些地方上段不接下段或者有的地方不完整,如果你看著比較費力或者有什麼地方有問題,都可以在評論區告訴我,我會及時修改的。當然啦,多執行緒的東西也不止這些,題目也就只是個題目,不要當真。想要了解更多的東西,還得自己去網上挖掘相關資料。多看看官方文件。實在是編不下去了,大家好好看~。對了,看我寫的這麼賣力,不打賞的話得點個喜歡也是極好的。