iOS多線程之GCD

本篇文章是iOS多線程系列的第二篇文章,之所以將GCD放在第二篇介紹,是因為理解了GCD后就比較容易理解NSOperation,NSOperation是蘋果對GCD的封裝的產物,以便我們開發中更好地使用。

本篇文章主要內容:

  • GCD是什么
  • 學習GCD之前需要理解的東西
  • 常見GCD函數的用法及實例演示

GCD是什么

GCD(Grand Central Dispatch),是libdispatch的市場名稱,是Apple開發的一個多線程編程的優化方案,Apple也推薦開發者使用此方案。libdispatch基于線程池的模式管理、執行并行任務。GCD大大降低了開發者維護線程的成本,使用起來也更加便捷、愉悅。

學習GCD之前需要理解的東西

俗話說:磨刀不誤砍柴工,理解多線程的相關知識對于深入學習GCD是十分必要的。作為計算機相關專業的學生,多線程的學習是基礎同時也是必不可少,其內容豐富,本篇文章只做大致回顧,不再深入探討。注:以下術語及概念為個人理解表述

串行 VS 并發

串行和并發,是用來描述任務與任務之間的相對關系。串行指的是當前任務執行時其他任務需要等待,等待當前任務完成后,才能繼續下一個任務,任務一個一個地執行;并發指的是當前任務執行的同時,其他任務也可以同時執行,不必等待當前任務的完成。大致可以用下圖表示:

串行和并發.png

我們可以看到,串行的情況下,不同的任務在任何時間段內都不會出現重疊執行,而并發的情況下,不同的任務在某個時間段內可能會存在重疊執行。

并發 VS 并行

并發的概念已經介紹過了,并行主要是指某個時刻CPU同時執行多個任務的狀態。嚴格意義上來說,多核CPU在不同的核上執行不同的任務,是真正的并行,對于單核CPU來說,通過上下文切換來使得不同的任務在一段時間內看起來也是被同時執行的。大致可以用下圖表示:

并行.png

我們可以看到,對于多核CPU,任務一和任務二在同時運行,對于單核CPU,任務一和任務二在交替運行。當然,這里只是舉個例子,并發代碼(并發任務)具體是否并行執行,完全取決于系統調度。并行一定需要并發,但并發不一定會并行。我們能夠決定哪些代碼需要并發,卻不能決定這些代碼真正并行。

串行隊列 VS 并發隊列

串行隊列中的任務,會按照提交順序一個一個執行,一個任務執行完成后,才能執行下一個任務;并發隊列中的任務,也會按照它們被添加的順序執行,但完成時機不確定,例如提交了任務一,再提交了任務二,并發隊列只能保證任務二在任務一開始執行后才執行,但任務二的結束時間可能比任務一早,也可能晚。

同步 VS 異步

同步表示當前線程會等待已提交的任務執行完成后繼續往后執行,異步表示當前線程提交任務后,直接繼續往后執行,不會等待已提交的任務,已提交的任務會在稍后的某個時間點完成。同步任務會阻塞當前線程,而異步任務不會

死鎖

死鎖表示兩個或多個線程相互等待而導致任何一個線程都不能執行。例如線程A等待線程B完成后才執行,線程B等待線程A完成后才執行,最終結果是A、B都不能執行。死鎖有點類似于OC中的循環引用,可以對比理解。

上下文切換

上下文切換是指一個線程切換到另外一個線程或進程時保存和恢復運行狀態的操作,需要一定的時間和資源開銷。

常見GCD函數的用法及實例演示

系統隊列類型

系統提供的隊列包括:主隊列(串行隊列)、全局調度隊列(按優先級分為background、low、default和high)、自定義串行隊列、自定義并發隊列。選擇合適的隊列執行合適的任務,是學習GCD的重點。注:以下所有示例完整代碼在這里

dispatch_async

 NSString *threadInfo = [NSString stringWithFormat:@"dispatch_async之前線程信息:%@\n\n", [NSThread currentThread]];
    [self fillTextInfo:threadInfo];
    
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        [[NSThread currentThread] setName:@"dispatch_async demo"];
        NSMutableString *resultStr = [NSMutableString string];
        [resultStr appendString:[NSString stringWithFormat:@"任務所在線程信息:%@\n\n", [NSThread currentThread]]];
        [resultStr appendString:@"耗時任務開始執行\n\n"];
        [NSThread sleepForTimeInterval:3.0];//模擬耗時操作
        [resultStr appendString:@"耗時任務執行完畢\n\n"];
        
        //在主線程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf fillTextInfo:resultStr];
        });
    });

    [self fillTextInfo:@"任務塊后的執行代碼\n\n"];

我們通過 dispatch_async 將Block中的代碼(任務)異步提交到優先級為Default的全局隊列中執行。運行代碼示例后,可以從結果看出,Block前的代碼執行后,立馬執行Block后的代碼,并沒有等待Block中的代碼執行,在之后的某個時刻,Block中的代碼執行完畢,才將更新UI的代碼(任務)提交到了主線程執行。下面的表格顯示了不同類型隊列使用dispatch_async的情況:

主隊列 全局隊列 自定義串行隊列 自定義并發隊列
如果更新UI的代碼不在主線程上,需要通過dispatch_async提交這些代碼到主隊列,以便在稍后的某個時刻會執行這些代碼,在非主線程更新UI會出現不可預料的bug 耗時的、非UI操作通過dispatch_async提交到全局隊列是不錯的選擇,全局隊列還包括系統任務,不只是我們自己的任務 如果想讓幾個任務在后臺順序執行,可以通過dispatch_async提交到自定義串行列 提交的任務會并發執行,任務之間可能會同時訪問某一份數據而引起數據損壞需要注意

dispatch_after

 [self fillTextInfo:@"準備執行dispatch_after\n\n"];
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf fillTextInfo:@"正在執行Block\n\n"];
    });
    
    [self fillTextInfo:@"dispatch_after后面代碼\n\n"];

上面的dispatch_after在延遲2秒后,將Block任務異步提交到主隊列中,Block后面的代碼先執行,Block里面的代碼后執行,即使延遲0秒也是這樣的執行順序,因為Block中的代碼會在主隊列中排隊,等到前面的任務結束后才會執行。 dispatch_after和dispatch_async的區別只是延遲提交了。兩者大同小異,這里就不詳細介紹了。

dispatch_sync

  NSLog(@"準備執行dispatch_sync");
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"正在執行Block");
        [NSThread sleepForTimeInterval:2.0];//模擬耗時操作
    });
    
    NSLog(@"dispatch_sync后面代碼");

上面代碼的執行順序是確定的:準備執行dispatch_sync —》正在執行Block —》dispatch_sync后面代碼。dispatch_sync使用場景是Block之后的代碼執行需要用到Block塊執行后的結果,有前后關系或者叫依賴。但使用dispatch_sync要注意避免死鎖。下面的代碼就是死鎖:

    NSLog(@"準備執行dispatch_sync");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"正在執行Block");
        [NSThread sleepForTimeInterval:2.0];//模擬耗時操作
    });
    
    NSLog(@"dispatch_sync后面代碼");

下面的表格顯示了串行隊列和并發隊列使用dispatch_sync的注意事項:

串行隊列 并發隊列
如果在某個串行隊列(不管是主隊列還是自定義串行隊列)向本隊列提交了同步任務,一定會產生死鎖,要慎重 比較適合使用dispatch_sync

dispatch_once

單例模式是一種常用的軟件設計模式。通過單例模式可以保證系統中一個類只有一個實例。在iOS開發中,dispatch_once是最完美的方案,且效率很高。直接看代碼:

@implementation NBLPhotoManager

+ (instancetype)sharedManager
{
    static NBLPhotoManager *_manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _manager = [[self alloc] init];
    });
    
    return _manager;
}

@end


for (NSInteger i = 0; i < 5; i++) {
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NBLPhotoManager *photoManager = [NBLPhotoManager sharedManager];
            NSString *tmpStr = [NSString stringWithFormat:@"%@\n\n", photoManager];
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf fillTextInfo:tmpStr];
            });
        });
    }

不論創建多少個對象,它們的地址信息都是一樣的,說明是同一個對象。dispatch_once 使Block中的代碼只能執行一次,且線程安全。

Dispatch barrier

- (dispatch_queue_t)concurrentQueue
{
    if (!_concurrentQueue) {
        dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.neebel.GCDDemoBarrier", DISPATCH_QUEUE_CONCURRENT);
        _concurrentQueue = concurrentQueue;
    }

    return _concurrentQueue;
}

#pragma mark - Action

- (void)start
{
    for (NSInteger i = 0; i < 3; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"任務%@", [NSNumber numberWithInteger:i].stringValue);
        });
    }
    
    dispatch_barrier_async(self.concurrentQueue, ^{
        NSLog(@"任務barrier");
    });
    
    for (NSInteger i = 3; i < 6; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"任務%@", [NSNumber numberWithInteger:i].stringValue);
        });
    }
    
}

代碼中我們提交了三個異步任務到自定義的并發隊列中,然后異步提交了一個障礙任務,最后又提交了三個異步任務,從執行結果上可得知,不管前面三個和后面三個任務各自的執行順序如何,障礙任務總是在前三個任務執行之后執行,在后三個任務執行之前執行。障礙任務執行期間,其他任務都不會執行,在這段時間內,相當于串行隊列。dispatch_barrier_sync的用法大家自行研究。下面表格顯示了dispatch_barrier的使用場景:

串行隊列 全局隊列 自定義并發隊列
串行隊列不用dispatch_barrier,因為本來就是串行的 最好不要用,會阻塞到系統任務 比較適合使用

Dispatch group

- (void)startBlockGroup
{
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        [NSThread sleepForTimeInterval:1.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
        NSLog(@"任務1完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        [NSThread sleepForTimeInterval:2.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
        NSLog(@"任務2完成");
        dispatch_group_leave(group);
    });
    
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"所有任務完成");
}

任務一和二不管完成順序如何,NSLog(@"所有任務完成") 是在兩個任務都完成之后才能執行。dispatch_group_wait的方式會阻塞當前線程。

- (void)startUnBlockGroup
{
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        [NSThread sleepForTimeInterval:1.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
        NSLog(@"任務1完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        [NSThread sleepForTimeInterval:2.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
        NSLog(@"任務2完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任務完成");
    });
    
    NSLog(@"非阻塞所以會先打印這句話");
}

上面的方式不會阻塞當前線程,所以經常會用這種方式。

dispatch_apply

- (void)start
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_apply(2, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
        switch (i) {
            case 0:
            {
                dispatch_group_enter(group);
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
                    [NSThread sleepForTimeInterval:1.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
                    NSLog(@"任務1完成");
                    dispatch_group_leave(group);
                });
            }
                break;
                
            case 1:
            {
                dispatch_group_enter(group);
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
                    [NSThread sleepForTimeInterval:2.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
                    NSLog(@"任務2完成");
                    dispatch_group_leave(group);
                });
            }
                break;
                
            default:
                break;
        }
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任務完成");
    });
}

dispatch_apply類似于for循環,但它的迭代是并發執行的,而for循環是順序執行的。使用時要考慮其資源開銷值不值得。

信號量

信號量機制比較復雜,用處也很多,例如經典的哲學家進餐問題。深入理解信號量機制需要大家花費更多的時間和精力研究。下面的代碼使用信號量解決線程安全問題,希望能起到拋磚引玉的作用。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"信號量";
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.startButton];
    [self.view addSubview:self.infoTextView];
    semaphore = dispatch_semaphore_create(1);
}

- (void)start
{
    __weak typeof(self) weakSelf = self;
    for (NSInteger i = 0; i < 5; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSObject *object = [weakSelf buildAnObj];
            NSLog(@"%@", object);
        });
    }
    
    for (NSInteger i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NSObject *object = [weakSelf buildAnObj];
            NSLog(@"%@", object);
        });
    }
}
//目的是只創建一個對象
- (NSObject *)buildAnObj
{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (!self.obj) {//這個判斷在多線程訪問時是不安全的,可能存在多個線程同時進入執行的情況,使用信號量機制充當鎖就沒問題了
        self.obj = [[NSObject alloc] init];
    }
    dispatch_semaphore_signal(semaphore);
    
    return self.obj;
}

上面的代碼無論執行多少次都只會創建一個對象,原因是dispatch_semaphore_create(1),創建了一個值為1的信號量,當一個線程A執行了dispatch_semaphore_wait后,信號量的值會減1,變為0,這時候其他線程就會等待,等到A執行dispatch_semaphore_signal后,信號量才會加1,其他線程才會繼續執行,作用類似于線程鎖,從而保證了線程安全。

至此,iOS多線程之GCD就介紹完了,文中沒有用到很多專業解釋,都是根據自己的理解表述的,目的是便于大家理解,有不妥或不正確的還請指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 多線程 在iOS開發中為提高程序的運行效率會將比較耗時的操作放在子線程中執行,iOS系統進程默認啟動一個主線程,用...
    郭豪豪閱讀 2,608評論 0 4
  • 1. GCD簡介 什么是GCD呢?我們先來看看百度百科的解釋簡單了解下概念 引自百度百科:Grand Centra...
    千尋_544f閱讀 393評論 0 0
  • 原創文章 轉載請注明出處, 謝謝! (~ o ~)Y 本文思維導圖 GCD是什么 全稱是 Grand Centra...
    Jimmy_P閱讀 4,697評論 10 67
  • GCD (Grand Central Dispatch) :iOS4 開始引入,使用更加方便,程序員只需要將任務添...
    池鵬程閱讀 1,344評論 0 2
  • iOS多線程之GCD 什么是GCD GCD(grand central dispatch) 是 libdispat...
    comst閱讀 1,216評論 0 0