最淺顯易懂的iOS多線程技術 - GCD的教程

FZKDel 7年前發布 | 9K 次閱讀 iOS開發 移動開發

筆者勵志打造一篇淺顯易懂地介紹iOS中GCD的文章!

筆者見過很多其他講解GCD的博客,有些寫得非常詳細非常專業,幾乎涵蓋了GCD大大小小的全部知識,細致龐雜的內容容易讓人摸不清主次,筆者覺得這類文章 并不適合初學者學習 ,于是決定寫一篇針對一些只是聽過,但是對GCD還不了解的童鞋們。

本文排除了一些細枝末節,擾亂人頭緒的東西,著重講解了GCD中重要的知識點,并在最后展示了GCD中 經常使用的函數 并附上結果圖和講解,簡單明了。

進程與線程

在了解多線程之前,需要弄清進程和線程的概念和他們之間的區別。

進程:

系統中正在運行的一個程序,進程之間是相互獨立的,每個進程都有屬于自己的內存空間。比如手機中的 微信 應用和 印象筆記 應用,他們都是iOS系統中獨立的進程,有著自己的內存空間。

線程:

進程內部執行任務所需要的執行路徑。進程若想執行任務,則必須得在線程下執行。也就是說進程至少有一個線程才能執行任務。但是,我們使用軟件的時候,很少有只讓它做一件事的時候:

舉個 印象筆記 的:chestnut: : 當你正在編輯一則筆記的時候點擊了同步按鈕,那么編輯任務(線程)和同步任務(線程)一定是不能按照順序執行的。因為同步任務的完成時間是不可控的,如果在同步的過程中無法進行別的任務(線程)那就太糟糕了!

因此,我們需要讓一些任務可以同時進行。既然任務是在線程上執行的,那么多任務的執行就意味著需要多線程的開啟和使用。

來一張圖直觀地展示一下內存,進程和線程的關系:

內存,進程和線程

多線程概述

多線程的實現原理:雖然在同一時刻,CPU只能處理1條線程,但是CPU可以快速地在多條線程之間調度(切換),造成了多線程并發執行的假象。

1. 多線程的優點

  • 能適當提高程序的執行效率。
  • 能適當提高資源利用率(CPU、內存利用率)。

2. 多線程的缺點

  • 創建線程是需要成本的:iOS下主要成本包括:在棧空間的子線程512KB、主線程1MB,創建線程大約需要90毫秒的創建時間。
  • 線程越多,CPU在調度線程上的開銷就越大。
  • 線程越多,程序設計就越復雜:因為要考慮到線程之間的通信,多線程的數據共享。

多線程在iOS開發中的應用

1. iOS的主線程

一個iOS程序運行后,默認會開啟1條線程,稱為“主線程”或“UI線程”

主線程的作用:

  • 顯示\刷新UI界面
  • 處理UI事件(比如點擊事件、滾動事件、拖拽事件等)

主線程的使用 注意事項 :

不能把比較耗時的操作放到主線程中,,嚴重影響UI的流暢度,給用戶一種程序“卡頓”的體驗。

因此,要將耗時的操作放在子線程中異步執行。這樣一來,及時開始執行了耗時的操作,也不會影響主線程中UI交互的體驗。

2. iOS的子線程

子線程是異步執行的,不影響主線程。在iOS開發中,我們需要將耗時的任務(網絡請求,復雜的運算)放在子線程進行,不讓其影響UI的交互體驗。

3. 多線程安全

當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。就好比好幾個人在同時修改同一個表格,造成數據的錯亂。

3.1 資源搶奪的解決方案

我們需要給數據添加 互斥鎖 。也就是說,當某線程訪問一個數據之前就要給數據加鎖,讓其不被其他的線程所修改。就好比一個人修改表格的時候給表格設置了密碼,那么其他人就無法訪問文件了。當他修改文件之后,再講密碼撤銷,第二個人就可以訪問該文件了。

注意:

這里的線程都為子線程,如果給數據加了鎖,就等于將這些異步的子線程變成同步的了,這也叫做線程同步技術。

3.2 互斥鎖使用:

@synchronized(鎖對象) { // 需要鎖定的代碼  };

3.3 互斥鎖的優缺點

優點:能有效防止因多線程搶奪資源造成的數據安全問題

缺點:需要消耗大量的CPU資源

互斥鎖的使用前提:多條線程搶奪同一塊資源的時候使用。

3.4互斥鎖在iOS開發中的使用

OC在定義屬性時有 nonatomic 和 atomic 兩種選擇

  • atomic:原子屬性,為setter方法加鎖(默認就是atomic)
  • nonatomic:非原子屬性,不會為setter方法加鎖

3.5 nonatomic和atomic對比

atomic:線程安全,需要消耗大量的資源

nonatomic:非線程安全,適合內存小的移動設備

建議:

所有屬性都聲明為nonatomic,盡量避免多線程搶奪同一塊資源,將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力。

多線程在iOS中的應用:GCD

GCD,全稱為 Grand Central Dispatch ,是iOS用來管理線程的技術。 純C語言,提供了非常多強大的函數。

1. GCD的優勢

GCD會自動利用更多的CPU內核(比如雙核、四核)。

GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)。

程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼。

2. 為什么要用GCD?

為了要提高軟件性能,應該異步執行耗時任務(加載圖片),以防止影響主線程任務的執行(UI相應)。

舉個:chestnut: :

從網絡加載一張圖片,如果將此任務放到主線程,那么在下載完成的時間里,軟件是無法相應用戶的任何操作的。特別地,如果當前是在可以滾動的頁面,就會造成無法滾動這種體驗非常糟的情況。

所以:應該將網絡加載放在異步執行,執行成功后,再回到主線程顯示加載后的圖片(詳細做法馬上就會講到)。

3. GCD的使用步驟

  1. 由開發者定制將要執行的任務。
  2. 將任務添加到隊列中,GCD會自動將隊列中的任務取出,放到對應的線程中執行。

注意:

任務的取出遵循隊列的FIFO原則:先進先出,后進后出。

4. 什么是隊列?

隊列是用來存放任務的,由GCD將這些任務從隊列中取出并放到相應的線程中執行。

GCD的隊列可以分為2大類型:

1. 并發隊列(Concurrent Dispatch Queue)

可以讓多個任務并發(同時)執行(自動開啟多個線程同時執行任務),并發功能只有在異步(dispatch_async)函數下才有效

2. 串行隊列(Serial Dispatch Queue)

讓任務一個接著一個地執行(一個任務執行完畢后,再執行下一個任務)。

那么隊列和線程又有什么區別?

簡單來說,隊列就是用來存放任務的“暫存區”,而線程是執行任務的路徑,GCD將這些存在于隊列的任務取出來放到相應的線程上去執行,而隊列的性質決定了在其中的任務在哪種線程上執行。

下面由一張圖來直觀地展示任務,隊列和線程的關系:

任務,隊列和線程

在這里,我們可以看到,放入串行隊列的任務會一個一個地執行。而放入并行隊列的任務,會在多個線程并發地執行。

5. 隊列的創建

5.1 串行隊列的創建:

GCD中獲得串行有2種途徑:

1.使用 dispatch_queue_create 函數創建串行隊列

// 創建串行隊列(隊列類型傳遞NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("serial_queue", NULL);

2.使用主隊列(跟主線程相關聯的隊列)

主隊列是GCD自帶的一種特殊的串行隊列:放在主隊列中的任務,都會放到主線程中執行。

可以使用dispatch_get_main_queue()獲得系統提供的主隊列:

dispatch_queue_t queue = dispatch_get_main_queue();

5.2 并發隊列的創建:

1.使用 dispatch_queue_create 函數創建并發隊列。

dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

2.使用 dispatch_get_global_queue 獲得全局并發隊列。

GCD默認已經提供了全局的并發隊列,供整個應用使用,可以無需手動創建。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

6. GCD的幾種重要的應用

6.1 子線程與主線程的通信

需求點:我們有時需要在子線程處理一個耗時比較長的任務,而且此任務完成后,要在主線程執行另一個任務。

例子:從網絡加載圖片(在子線程),加載完成就更新UIView(在主線程)。

為了實現這個需求,我們需要首先拿到全局并發隊列(或自己開啟一個子線程)來執行耗時的操作,然后在其完成block中拿到全局串行隊列來執行UI刷新的任務。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

              //加載圖片
              NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
              UIImage *imageFromData = [UIImage imageWithData:dataFromURL];

      dispatch_async(dispatch_get_main_queue(), ^{

              //加載完成更新view
              UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];          
      });      
  });

以筆者的拙見,除了復雜的算法,網絡請求以外,大多數 dataWithContentsOf。。。 函數可能也會比較耗時,所以以后遇到與NSData交互的操作時,盡量將其放在子線程執行。

6.2 dispatch_once

需求點:用于在程序啟動到終止,只執行一次的代碼。此代碼被執行后,相當于自身全部被加上了注釋,不會再執行了。

為了實現這個需求,我們需要使用 dispatch_once 讓代碼在運行一次后即刻被“雪藏”。

//使用dispatch_once函數能保證某段代碼在程序運行過程中只被執行1次

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只執行1次的代碼,這里默認是線程安全的:不會有其他線程可以訪問到這里
});

6.3 dispatch_group

需求點:執行多個耗時的異步任務,但是只能等到這些任務都執行完畢后,才能在主線程執行某個任務。

為了實現這個需求,我們需要讓將這些異步執行的操作放在 dispatch_group_async 函數中執行,最后再調用 dispatch_group_notify 來執行最后執行的任務。

dispatch_group_t group =  dispatch_group_create();
  dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      // 執行1個耗時的異步操作
  });

  dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      // 執行1個耗時的異步操作
  });

  dispatch_group_notify(group, dispatch_get_main_queue(), ^{
      // 等前面的異步操作都執行完畢后,回到主線程...
  });

讓我們看一下示例代碼和運行結果:

示例代碼:

為了使對比明顯,筆者多開了幾條線程,這樣更容易看清問題。

dispatch_group_t group =  dispatch_group_create();    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 執行1個耗時的異步操作
        for (NSInteger index = 0; index < 10000; index ++) {
        }
        NSLog(@"完成了任務1");                
    });

    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 執行1個耗時的異步操作
        for (NSInteger index = 0; index < 20000; index ++) {
        }
        NSLog(@"完成了任務2");        
    });

    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 執行1個耗時的異步操作
        for (NSInteger index = 0; index < 200000; index ++) {
        }
        NSLog(@"完成了任務3");

    });

    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 執行1個耗時的異步操作
        for (NSInteger index = 0; index < 400000; index ++) {
        }
        NSLog(@"完成了任務4");

    });

    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 執行1個耗時的異步操作
        for (NSInteger index = 0; index < 1000000; index ++) {
        }
        NSLog(@"完成了任務5");

    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

        // 等前面的異步操作都執行完畢后,回到主線程...
        NSLog(@"都完成了");        
    });

運行結果:

dispatch_group 的使用運行結果

從三次運行的結果來看:

  1. 異步執行的任務1-5的最終完成時間是與其自身完成任務所需要的時間并無絕對關聯。因為任務5是最耗時的,它在第一次運行結果里并不是最后才完成的。任務1是最不耗時的,但是它在第二次運行結果里也不是最先完成的。

  2. 異步執行的任務1-5無論完成順序如何,只有當他們都完成后才會調用主線程的打印“都完成了”。

6.4 dispatch_barrier

需求點:雖然我們有時要執行幾個不同的異步任務,但是我們還是要將其分成兩組:當第一組異步任務都執行完成后才執行第二組的異步任務。這里的組可以包含一個任務,也可以包含多個任務。

為了實現這個需求,我們需要使用 dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); 在兩組任務之間形成“柵欄”,使其“下方”的異步任務在其“上方”的異步任務都完成之前是無法執行的。

dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"----任務 1-----");
    });

    dispatch_async(queue, ^{
        NSLog(@"----任務 2-----");
    });    

    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----");
    });

    dispatch_async(queue, ^{
        NSLog(@"----任務 3-----");
    });

    dispatch_async(queue, ^{
        NSLog(@"----任務 4-----");
    });

示例代碼:

dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 10000; index ++) {
        }
        NSLog(@"完成了任務1");

    });

    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 20000; index ++) {
        }
        NSLog(@"完成了任務2");

    });

    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 200000; index ++) {
        }
        NSLog(@"完成了任務3");

    });


    dispatch_barrier_async(queue, ^{        
        NSLog(@"--------我是分割線--------");        
    });


    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 400000; index ++) {
        }
        NSLog(@"完成了任務4");

    });

    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 1000000; index ++) {
        }
        NSLog(@"完成了任務5");

    });

    dispatch_async(queue, ^{

        for (NSInteger index = 0; index < 1000; index ++) {
        }
        NSLog(@"完成了任務6");

    });

運行結果:

dispatch_barrier 的使用運行結果

從這三次運行結果來看:

  1. 無論任務1-3內部的執行順序如何,只有當三者都完成了才會執行任務4-6。
  2. 1-3內部的執行順序和4-6內部的完成順序都是不可控的,同上一個知識點類似。

本文介紹了需要了解GCD所需的最重要的知識,因為怕打斷讀者思路,并沒有涵蓋所有細節。

 

來自:http://www.jianshu.com/p/6e74f5438f2c

 

 本文由用戶 FZKDel 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!