GCD對于iOS開發者來說肯定不陌生,他和NSThread,NSOperation一起作為iOS開發中主要的三種多線程實作方法,而GCD是最最底層的,是以對于作為一個iOSer,GCD是必須掌握的。
我通過對于以下兩篇文章的閱讀,基本上掌握了GCD的基本使用方法。是以首先感謝兩位作者。
- GCD 深入了解:第一部分
- iOS多線程開發——GCD的使用與多線程開發淺析(二)
一、基本概念
對于新手來說,最常見同時最容易搞混的的莫過于GCD中的一些基本概念了。
- 并行與并發(Parallelism && Concurrency)
- 并行:顧名思義就是同時行動,兩個任務在兩個線程(Thread)上進行處理,彼此互不幹涉。而究其根本是因為多核進行處理,進而更快的解決任務。(可以類比于兩條水管中同時給遊泳池放水)
- 并發:顧名思義,同時發生。對于有的性能較差的機子,比如說那些隻有單核的裝置,為了讓使用者感覺能夠同時處理多個任務,他就需要通過不斷的切換正在處理的線程,進而實作一種“僞并行”,這樣就防止使用者因為一個任務進行太久而無法進行下一個任務。(可以類比于我用兩個口給遊泳池放水,不過因為隻有一根水管,而放水的方向有2個方面,是以需要不斷的切換,進而實作兩邊同時達到需要的水位)
- 區分如下圖:
- 串行與并發(Serial && Concurrent)
- 串行:由于所有任務在一個線程被執行的時候隻有一個任務會被執行,是以每個任務都要依賴于先來先處理(FIFO)原則,處理所有得到的内容。
- 并行:當接收到任務的時候,并行狀态下,他會開啟多個線程,将每個任務都配置設定到各個線程中,進而使得每個任務都能夠在同一時間被執行。這樣有效的減少了所消耗的時間。
- 區分圖如下:
- 隊列:就像前面所說,當你告訴電腦你需要執行哪些事情的時候,電腦就會把你告訴他的一件件事情放到自己的隊伍中,很明顯,先告訴他的事情放在前面,後告訴他的事情放在後面,即先進先處理(FIFO)原則處理所有事物,就像排隊一樣。
- 在GCD中主要的隊列有2種,就是前面提到的:
- 串行隊列
- 并發隊列
- 蘋果公司提供給我們的已存在的隊列有5種(不包括後期我們自己添加的内容)
- 主隊列(main queue):預設情況下所有事情都是在主隊列下進行處理,隻要有編碼經驗的人來說,主隊列很明顯是(串行隊列)。
- 全局排程隊列(Global Dispatch Queues):預設情況下全局隊列是系統提供的并發隊列的統稱,根據隊列的優先程度不同分為以下幾個(優先度從低到高):
- background
- low
- default
- high
- 在GCD中主要的隊列有2種,就是前面提到的:
-
同步與異步(synchronization && asynchronous)
-同步:同步指的是在原來的内容執行完成之後,再執行你下面所需要的任務。
-異步:異步指的是在和原來的内容執行的同時,在另一個地方同時處理新的任務。
二、基本使用
既然學了,那肯定要使用它。那麼我們先模拟一個環境:
這個程式裡面隻有主隊列,然後你想從網上下載下傳一個圖檔,放到你自己的手機裡面,然後使用者就可以對手機裡面的這張圖檔進行各種各樣的處理。
但是問題來了:我們都知道下載下傳需要時間,雖然現在已經是4G時代,下載下傳速度很快,但是如果這個圖檔很大,那麼他就需要很長的時間來進行等待。而在這段時間内,使用者什麼事情都不能幹,隻能默默的幹等着。
這很明顯是一個很差的使用者體驗
那麼既然我們已經知道問題所在,就需要想辦法解決它:
- 我們需要在使用者等待的時候添加一個動畫效果,告訴他我們正在下載下傳(這個和今天主題無關,我們就不繼續深入了解)
- 将下載下傳任務放到背景(如果仍舊放在前台的話,就算有動畫,動畫效果也不會動)
那麼這個時候就需要異步隊列的存在了
就像前面的基本概念中所提到的,執行分為同步執行和異步執行,任務隊列分為串行隊列和并發隊列。是以他們彼此一一結合,總共有4種情況存在,主要的彼此結合的狀态圖如下所示:
單隊列
串行隊列,同步執行
代碼如下:
func serialDispatchQueueWithSync() {
let queue = dispatch_queue_create("serialDispatchQueue", DISPATCH_QUEUE_SERIAL)
print("0")
dispatch_sync(queue) {
print("1")
}
dispatch_sync(queue) {
print("2")
}
dispatch_sync(queue) {
print("3")
}
dispatch_sync(queue) {
print("4")
}
dispatch_sync(queue) {
print("5")
}
print("6")
}
運作結果如下:
0
1
2
3
4
5
6
串行隊列,異步執行
代碼如下:
func serialDispatchQueueWithASync() {
let queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL)
print("0")
dispatch_async(queue) {
print("1")
}
dispatch_async(queue) {
print("2")
}
dispatch_async(queue) {
print("3")
}
dispatch_async(queue) {
print("4")
}
dispatch_async(queue) {
print("5")
}
dispatch_async(queue) {
print("6")
}
print("7")
}
運作結果如下:
0
7
1
2
3
4
5
6
解釋:因為queue是異步執行的,即調用dispatch_async函數,是以輸出1 2 3 4 5 6的時候是在另一個線程中的,是以和主線程中的輸出1 7 無關。
并發隊列,同步執行
代碼如下:
func concurrentDispatchsQueueWithSync() {
let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT)
print("0")
dispatch_sync(queue) {
print("1")
print("12")
print("13")
}
dispatch_sync(queue) {
print("2")
print("22")
print("23")
}
dispatch_sync(queue) {
print("3")
print("32")
print("33")
}
dispatch_sync(queue) {
print("4")
print("42")
print("43")
}
print("5")
}
運作結果如下:
0
1
12
13
2
22
23
3
32
33
4
42
43
5
并發隊列,異步執行
代碼如下:
func concurrentDispatchsQueueWithASync() {
let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT)
print("0")
dispatch_async(queue) {
print("1")
print("12")
print("13")
}
dispatch_async(queue) {
print("2")
print("22")
print("23")
}
dispatch_async(queue) {
print("3")
print("32")
print("33")
}
dispatch_async(queue) {
print("4")
print("42")
print("43")
}
print("5")
}
運作結果如下:
0
7
1
2
12
3
22
13
4
32
23
5
42
33
52
43
53
其他常見用法:
挂起與恢複
- 線程挂起:
dispatch_suspnd()
- 線程恢複:
dispatch_resume()
信号量
由于有的時候(比如說在建立添加數組中的對象的時候),因為不同線程操縱的是同一個對象,是以很容易發生報錯,這個時候需要通過信号量來對對應的内容進行控制,當信号量為0的時候,進入等待狀态,不能執行下面的内容;當信号量為1的時候,可以執行,同時信号量減一,等到執行完畢,信号量加一。
- 等待執行
dispatch_semaphore_wait()
- 信号量加一
dispatch_semahore_signal
隻執行一次
有的時候,有的東西的建立隻能被建立一次(即單例),這個時候就需要用到
dispatch_once()
代碼如下:
var token: dispatch_once_t = 0
func test() {
dispatch_once(&token) {
println("This is printed only on the first call to test()")
}
println("This is printed for each call to test()")
}
關于讀寫問題的解決方法(買票問題)
由于日常編碼的過程中經常會遇到,當你在調用這個這個變量的時候。另一個線程也在調用這個變量(隻在并發過程中),如果兩個線程都是在讀取資料,那并沒有什麼問題,但是如果其中一個在寫入,或者兩個都在寫入,那麼就會出現很大問題,是以蘋果官方在GCD中也給我們準備了一個方法,讓我們解決這個問題,那就是
dispatch_barrier_async
,這個方法讓對的内容在并發過程中加入,進而友善組織内容的修改,進而使得對應的内容隻能在目前線程中被進行修改。
常見問題:
死鎖:
let queue = dispatch_get_main_queue()
dispatch_async(queue) {
dispatch_sync(queue, {
print("1")
})
}
産生原理:因為
dispatch_sync()
會等到本身結束之後才會在主線程繼續執行接下去的代碼,但是
dispatch_sync()
這個方法調用的就是主線程,是以午飯等到主線程結束,是以就無法傳回,就會卡在這裡。
解釋:由于是并發隊列,是以他會建立多個線程,進而保證每個線程的任務都能夠盡快完成,是以順序有一定的出入。
最後通過兩張動态圖來最後總結單隊列:
dispatch_sync
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
- 主隊列按照預定的順序下來
- viewDidLoad在主線程進行執行。
- 直到執行到
dispatch_sync
- 調用
代碼,将block添加到全局隊列中,主隊列挂起。dispatch_sync
- 全局隊列先完成之前存放在全局隊列中的内容。
- 完成之前的任務後,執行
的block中的内容。dispatch_sync
- 完成block中的任務,主隊列上的任務得以恢複
- 主隊列繼續執行其他任務。
dispatch_async
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
![dispatch_sync](https://camo.githubusercontent.com/2c7cbaf76001a56622e14cf48a8d914d4b5c9df4/687474703a2f2f63646e312e72617977656e6465726c6963682e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031342f30312f64697370617463685f6173796e635f696e5f616374696f6e2e676966)
- 主隊列一路按順序執行任務——接着是一個執行個體化
的任務,其中包含了UIViewController
。viewDidLoad
-
在主線程執行。viewDidLoad
- 主線程目前在
内,正要到達viewDidLoad
。dispatch_async
-
被添加到一個全局隊列中,将在稍後執行。dispatch_async Block
-
在添加viewDidLoad
到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列并發地處理它未完成地任務。記住dispatch_async
在全局隊列中将按照(FIFO)順序出列,但可以并發執行。Block
- 添加到
的代碼塊開始執行。dispatch_async
-
完成,兩個dispatch_async Block
語句将它們的輸出放在控制台上。NSLog
排程組
既然能夠處理一個個的任務,那麼我們就繼續模拟一個環境,當我們需要在網上下載下傳内容的時候(這些内容需要彼此聯系在一起才能正常使用),這個時候,上面的單隊列就不夠了(或者說如果使用單隊列産生的效果不是時間太長,就是檔案的完整性不夠好)
這個時候我們就需要引入多隊列,當多個内容都處理完成之後,讓系統告訴我們,我們已經完成了以上的下載下傳。可以繼續做下一步事情了。
任務開始
在GCD中,我們可以通過
dispatch_group_enter
來通知目前任務的開始,而與之相對應的,我們必須在任務完成後,手動通知排程組任務結束(
dispatch_group_leave
)這樣才能讓排程組知道我們這個任務已經結束。
代碼如下(由于demo中有一個Photo類,是以此處貼上OC代碼):
dispatch_group_enter(downloadGroup); // 3
Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 4
}];
[[PhotoManager sharedManager] addPhoto:photo];
任務提醒
當我們所有的任務都手動通知後,那麼就需要條用提醒來告訴他(我已經完成了所有内容,接下去需要試試哪裡能夠執行了),而提醒方式在GCD中有兩種:
-
dispatch_group_wait() + dispatch_async()
-
dispatch_group_notify
其他常見用法
dispatch_apply()
有的時候需要調用for循環來反複執行,但是當需要執行的代碼量偏大的時候,for的效率比較低,這個時候需要用
dispatch_apply()
來執行,這樣節省效率,不過當需要執行的代碼量比較小的時候,
dispatch_apply()
的效率就比較差了。
這個方法的效果和 dispatch_sync
一樣,是以要注意死鎖(後面會提到)