iOS 多線程 淺述

什么是進程?

  • 進程是指在系統中正在運行的一個應用程序。
  • 每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內。

什么是線程?


  • 1個進程要想執行任務,必須得有線程(每1個進程至少要有1條線程)。
  • 線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行。

小拓展

- 線程的串行(就像烤串一樣)
    - 1個線程中任務的執行是串行的。
    - 如果要在1個線程中執行多個任務,那么只能一個一個地按順序執行這些任務。
    - 在`同一時間內`,1個線程只能執行1個任務。

什么是多線程?

  • 1個進程中可以開啟多條線程,每條線程可以并行(同時)執行不同的任務。

  • 線程的并行(同時執行)

    • 比如同時開啟3條線程分別下載3個文件(分別是文件A、文件B、文件C。
  • 多線程并發執行的原理:

    • 在同一時間里,CPU只能處理1條線程,只有1條線程在工作(執行)。
    • 多線程并發(同時)執行,其實是CPU快速地在多條線程之間調度(切換),如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象。(如下圖)
    CPU調用線程

多線程優缺點:

  • 優點
    • 能適當提高程序的執行效率。
    • 能適當提高資源利用率(CPU、內存利用率)
  • 缺點
    • 開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能。
    • 線程越多,CPU在調度線程上的開銷就越大。
    • 程序設計更加復雜:比如線程之間的通信、多線程的數據共享

多線程在iOS開發中的應用

- 主線程
    - 一個iOS程序運行后,默認會在自己的進程中開啟1條線程,稱為“主線程”也叫“UI線程”。
    - 作用:刷新顯示UI,處理UI事件。
- 使用注意
    - 不要將耗時操作放到主線程中去處理,因為會卡住主線程,造成UI卡頓(用戶體驗差)。
    - 和UI相關的刷新操作`必須`放到主線程中進行處理。

線程的狀態

  • 線程的各種狀態:新建-就緒-運行-阻塞-死亡
  • 常用的控制線程狀態的方法
        [NSThread exit];//退出當前線程
        [NSThread sleepForTimeInterval:7.0];//阻塞線程
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:7.0]];//阻塞線程
    
    

    注意:線程死亡后不能復生


線程安全:

  • 前提:多個線程同時訪問同一塊資源會發生數據安全問題 解決方案:加互斥鎖
  • 相關代碼:@synchronized(self){}
  • 專業術語-線程同步
  • 原子和非原子屬性(是否對setter方法加鎖)

IOS中多線程的實現方案

方案 簡介 語言 線程生命周期 使用頻率
pthread 一套通用的多線程API
(跨平臺\可移植)
C語言 程序員管理 幾乎不用
NSThread 使用更加面向對象
(簡單易用,可直接操作線程對象)
OC語言 程序員管理 偶爾使用
GCD 為了替代NSThread為生
充分利用設備多核
C語言 系統自動管理 經常使用
NSOperation 基于GCD
更加面向對象 更方便地設置線程之間的依賴 監聽線程狀態KVO
OC語言 系統自動管理 經常使用

pthread簡單使用

1.包含頭文件(必須)

#import <pthread.h>

2.創建線程

//  創建線程
/**
     *
     * 參數一:線程對象(傳地址)
     * 參數二:線程的屬性(名稱\優先級)
     * 參數三:只想函數的指針
     * 參數四:函數需要接受的字符串參數,可以不傳遞(注:由于我們創建的是OC的字符串,所以在傳值的時候需要將其轉換成C的字符串)
     */
    pthread_t thread;
    NSString *num = @"123";
    pthread_create(&thread, NULL, task, (__bridge void *)(num));

3.定義參數所需要的函數指針


void *task(void *num)
{
    NSLog(@"當前線程 -- %@,傳入的參數:-- %@", [NSThread currentThread], num);

    return NULL;
}

如果需要退出線程的話只需調用下面代碼

pthread_exit(NULL);

運行結果:


pthread線程使用截圖

NSThread簡單使用

這邊介紹NSThread創建線程的4種方式:

  • 第一種 (alloc nitWithTarget:selector:object:)
    • 特點:需要手動開啟線程,可以拿到線程對象進行詳細設置
    • 優缺點:
      • 缺點:需要手動開啟線程執行任務
      • 優點:可以拿到線程對象
//  創建線程
    /**
     * 參數一:目標對象
     * 參數二:方法選擇器(線程啟動后調用的方法)
     * 參數三:調用方法需要接受的參數
     */
    NSThread *thread = [[NSThread alloc] initWithTarget:self
                                               selector:@selector(task)
                                                 object:nil];

    //  開始執行
    [thread start];
  • 第二種(分離出一條子線程)
    • 特點:自動啟動線程,無法對線程進行更詳細的設置
    • 優缺點:
      • 缺點:無法拿到線程對象 進行更詳細設置
      • 優點:代碼簡單且自動開啟線程執行
//  創建線程
    /**
     * 參數一:要調用的方法
     * 參數二:目標對象 self
     * 參數三:調用方法需傳遞的參數
     */
    [NSThread detachNewThreadSelector:@selector(task)
                             toTarget:self
                           withObject:nil];
  • 第三種(后臺線程)
    • 特點:自動啟動線程,無法進行更詳細設置
    • 優缺點:
      • 缺點:無法拿到線程對象 進行更詳細設置
      • 優點:代碼簡單且自動開啟線程執行

/**
 *  NSThread創建一條后臺線程
 */
- (void)nsthreadTest3
{
    //  創建線程
    /**
     * 參數一:要調用的方法
     * 參數二:調用方法需傳遞的參數
     */
    [self performSelectorInBackground:@selector(run:) withObject:@"后臺線程"];

}

- (void)run:(NSString *)str
{
    NSLog(@"當前線程:%@ -- 接收到的參數:%@", [NSThread currentThread], str);
}

  • 第四種(自定義NSThread類并重寫內部的方法實現)
    • 特點:可以不暴露一些實現細節,使代碼增加隱蔽性。(一般出現在第三方框架內)
    • 優缺點:
      • 缺點:繁瑣,且需要手動開啟線程執行
      • 優點:增加代碼隱蔽性

1.創建自定義類繼承自NSThread
2.重寫NSThread類中的main方法

- (void)main
{
    NSLog(@"當前線程--%@", [NSThread currentThread]);
}

3.創建線程對象

/**
 *  NSThread創建一條后臺線程
 */
- (void)nsthreadTest4
{
    //  創建線程
    SJThread *thread = [[SJThread alloc] init];

    //  開啟執行
    [thread start];
}

線程間通信

有時候我們會從服務器上下載圖片然后再展示出來,下載的操作我們會放到子線程,而UI刷新的操作只能在主線程中執行。這樣就涉及到線程間的通信。接下來我們分三種方式來簡單實現一下:

  • 方式一:
- (void)viewDidLoad {
    [super viewDidLoad];

    // 開啟一條線程下載圖片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];

}


- (void)downloadImage
{
    //  網絡圖片url
    NSURL *url = [NSURL URLWithString:@"http://img3.imgtn.bdimg.com/it/u=3841157212,2135341815&fm=206&gp=0.jpg"];
    //  根據url下載圖片數據到本地
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    //  把下載到本地的二進制數據轉成圖片
    UIImage *image = [UIImage imageWithData:imageData];
    //  回到主線程刷新UI
    //  第一種方式
    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];

    //  第二種方式
    //  直接調用iconView里面的setImage:方法就可以實現刷新
//    [self.iconView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];

    //  第三種方式
    //  此方法可以方便自由在主線程和其它線程切換
//    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}

- (void)showImage:(UIImage *)image
{
    self.iconView.image = image;
}


GCD簡單使用

什么是GCD

  • GCD全稱是Grand Central Dispatch(牛逼的中樞調度器)
  • 純C語言,提供了非常多強大的函數

GCD優勢

  • GCD是蘋果公司為多核的并行運算提出的解決方案
  • GCD會自動利用更多的CPU內核
  • GCD會自動關了線程生命周期(創建、調度、銷毀線程)
  • GCD性能很好(接近底層)

GCD的組合方式

  • 異步函數+并發隊列:開啟多條線程,并發執行任務
  • 異步函數+串行隊列:開啟一條線程,串行執行任務
  • 同步函數+并發隊列:不開線程,串行執行任務
  • 同步函數+串行隊列:不開線程,串行執行任務
  • 異步函數+主隊列:不開線程,在主線程中串行執行任務
  • 同步函數+主隊列:不開線程,串行執行任務(注意死鎖發生

注意同步函數和異步函數在執行順序上面的差異

GCD的任務和隊列

  • 任務:執行什么操作
  • 隊列:用來存放任務(GCD中提供了2種隊列)
    • 串行隊列
    • 并發隊列

GCD的使用

  • 定制任務 —— 確定需要做的操作
  • 將任務添加到隊列中
  • GCD會自動將隊列中的任務取出,存放到線程中執行
  • 任務的取出遵循隊列的FIFO原則(先進先出,后進后出)
FIFO原則圖片

GCD創建線程

  • 接下來看看同步函數和異步函數有什么區別:

1.先來看看異步并發隊列

- (void)test
{
    /**
     * 參數一:C語言的字符串,給隊列起一個名字或標識
     * 參數二:隊列類型
        DISPATCH_QUEUE_CONCURRENT   并發
        DISPATCH_QUEUE_SERIAL   串行
     */
    dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);

    /**
     *  使用函數封裝任務
     * 參數一:獲取隊列
     * 參數二:需要執行的任務
     */
    dispatch_async(queue, ^{
        NSLog(@"在:%@線程執行了任務",[NSThread currentThread]);
    });

    NSLog(@"結束");
}

執行結果:


異步并發隊列截圖

2.再來看看同步并發隊列

- (void)test
{
    /**
     * 參數一:C語言的字符串,給隊列起一個名字或標識
     * 參數二:隊列類型
        DISPATCH_QUEUE_CONCURRENT   并發
        DISPATCH_QUEUE_SERIAL   串行(串行隊列可以用NULL表示)
     */
    dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);

    /**
     *  使用函數封裝任務
     * 參數一:獲取隊列
     * 參數二:需要執行的任務
     */
    dispatch_sync(queue, ^{
        NSLog(@"在:%@線程執行了任務",[NSThread currentThread]);
    });

    NSLog(@"結束");
}

執行結果:


同步并發隊列截圖

結論:

從上面的2個運行結果的時間可以看出
1.異步并發隊列,會開啟一條子線程來處理任務,以達到主線程和子線程同時執行的并發效果。
2.同步并發隊列,不會開線程,必須等block塊中的代碼先執行完畢才會繼續執行以外的任務,所以并發隊列對于同步函數來說等同于“無效”

  • 再看看并發隊列對異步函數和同步函數的影響:

1.同步函數+并發隊列

dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

執行結果:同步函數+并發隊列沒有開啟子線程的能力

并發隊列對同步函數的影響

2.異步函數+并發隊列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("并發隊列", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });
}

執行結果:異步函數+并發隊列會自動開啟3條子線程執行任務

并發隊列對異步函數的影響

結論:

從上面可以看出,異步函數擁有開啟子線程的能力,而同步函數沒有開啟子線程的能力。


  • GCD中,除了并發隊列外,還有串行隊列,我們來看看如果把并發隊列換成串行隊列會有怎樣的變化

1.同步函數+串行隊列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });
}

執行結果:進一步證明同步函數沒有開啟子線程的能力,他的所有任務都在主線程中執行

同步函數+串行隊列

2.異步函數+串行隊列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@",[NSThread currentThread]);
    });
}

執行結果:開啟了一條子線程,在子線程中依次執行任務

異步函數+串行隊列

結論

1.在同步函數+串行隊列中,任務依舊是在主線程中執行。

2.在異步函數+串行隊列中,會自動開啟一條子線程,在子線程中依次執行任務

3.再一次證明同步函數沒有開啟子線程的能力


系統提供的4個全局并發隊列

  • 在iOS中系統默認給我們提供了4個全局并發隊列
- (void)test3
{
    //  獲取全局并發隊列
    //  系統內部默認提供4個全局并發隊列
    /**
     * 參數一:優先級
     * 參數二:時間(傳0即可)
     */
//優先級:DISPATCH_QUEUE_PRIORITY_HIGH 2
//      DISPATCH_QUEUE_PRIORITY_DEFAULT 0
//      DISPATCH_QUEUE_PRIORITY_LOW (-2)
//      DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN    級別最低

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue, ^{
        NSLog(@"1當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"2當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"3當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"4當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"5當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"6當前線程:%@", [NSThread currentThread]);
    });
}

執行結果:在結果中我們看到GCD創建了6條線程,但是實際上GCD創建多少條線程完全由系統當前情況而定,我們是無法控制的。

獲取全局隊列

特殊的串行隊列 —— 主隊列(與主線程相關聯的隊列)

  • 主隊列是GCD自帶的一種特殊的串行隊列
  • 放在主隊列中的人物,都會放到主線程中執行
  • 使用dispatch_get_main_queue()的方式可獲取主隊列
    • 特點
      • 1.放在主隊列中的任務,必須在主線程中執行
      • 2.主隊列執行任務的時候,在調度任務的時候,會先調用主線程的狀態,如果當前有任務在做,則會等待主線程執行完任務再執行自己的任務

1.主隊列+異步函數

- (void)test4
{
    //  獲取主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任務
    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });
}

執行結果:任務都在主線程中執行

主隊列+異步函數

2.同步函數+主隊列

- (void)test4
{
    //  獲取主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任務
    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });
}

執行結果:
進入死鎖狀態,因為主隊列執行任務的時候,在調度任務的時候,會先調用主線程的狀態,如果當前有任務在做,則會等待主線程執行完任務再執行自己的任務

如果要解決以上的情況,那么可以將任務添加到子線程中,這樣就不會出現死鎖的情況,程序也就能夠正常執行了


[self performSelectorInBackground:@selector(test4) withObject:nil];

- (void)test4
{
    //  獲取主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任務
    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"當前線程:%@", [NSThread currentThread]);
    });
}

執行結果:

死鎖解決方式

總結

函數類型 并發隊列 手動創建的串行隊列 主隊列
同步 (sync) 1.沒有開啟新線程
2.串行執行任務
1.有開啟新線程
2.串行執行任務
死鎖
異步(async) 1.有開啟新線程
2.并發執行任務
1.有開啟新線程
2.串行執行任務
1.沒有開啟新線程
2.串行執行任務

注意

使用sync函數往當前串行隊列中添加任務,會卡主當前的串行隊列。

GCD線程間的通信

  • 有時候我們需要在子線程進行一些耗時操作,等耗時操作完成后再回到主線程進行相應的UI刷新,那么就可以使用下面的方式在子線程和主線程之間進行通信

- (void)test5
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_async(queue, ^{

        NSLog(@"在%@線程中執行任務", [NSThread currentThread]);
        //  回到主線程
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"在%@線程中執行任務", [NSThread currentThread]);
        });
    });
}

執行結果:


線程間通信

GCD延遲執行

  • 特點:可以選擇在哪個線程中執行任務
- (void)test6
{
    NSLog(@"方法開始運行");
    /**
     *  GCD延遲執行方法
     *
     *  參數一: 要延遲的時間 (以秒為單位)
     *  參數二: 在哪個線程中執行
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"GCD定時器");
    });
}

執行結果:

GCD延遲執行

一次性代碼

  • 特點:
    • 能保證整個程序運行過程中,block內的代碼塊只會被執行一次
    • 線程是安全的
    • 應用:簡單的單例模式(單例模式實現點我)
    • 注意點:不可放在懶加載中

- (void)test8
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"一次性代碼運行");
    });
}


柵欄函數

  • 作用:能夠控制并發隊列里面任務的執行順序
  • 注意:不能使用全局并發隊列(會沒有任何區別,文檔中有注釋——只對自己創建的并發隊列有效)
- (void)test7
{
    //  創建隊列
    dispatch_queue_t queue = dispatch_queue_create(0, 0);

    dispatch_async(queue, ^{

        for (int i = 0; i<5; i++) {
            NSLog(@"1");
        }
    });
    dispatch_async(queue, ^{

        for (int i = 0; i<5; i++) {
            NSLog(@"2");
        }
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"進入柵欄函數");
    });

    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"3");
        }
    });

    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"4");
        }
    });
}

執行結果:

柵欄函數執行結果

GCD迭代開發(遍歷)

  • 一般我們傳統的遍歷方式如下,它的缺點就是在處理比較耗時的操作時效率較低,因為只在一個線程中執行任務。
    //  傳統的遍歷方式
    for (int i ; i< 10; i++) {
        NSLog(@"%d -- 當前線程%@", i, [NSThread currentThread]);
    }

執行結果:


這里寫圖片描述
  • 在GCD中,為我們提供了一個迭代函數,可以開啟子線程快速進行遍歷,這樣就可以大大提高效率,而且使用非常簡單。接下來使用迭代函數來進行文件復制的操作:
- (void)test9
{
    //  獲得文件原始路徑(上層文件夾得路徑)
    NSString *fromPath = @"/Users/yeshaojian/Desktop/test";

    //  獲得文件的目標路徑
    NSString *toPath = @"/Users/yeshaojian/Desktop/test2";

    //  得到文件路徑下面的所有文件
    NSArray *subpaths =  [[NSFileManager defaultManager] subpathsAtPath:fromPath];
    NSLog(@"文件名:%@",subpaths);

    //  獲取數組中文件的個數
    NSInteger count = subpaths.count;

    //  將要迭代的操作放到迭代函數內
    dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t index){
            //  拼接需要復制的文件的全路徑
            NSString *fromFullpath = [fromPath stringByAppendingPathComponent:subpaths[index]];
            //  拼接目標目錄的全路徑
            NSString *toFullpath = [toPath stringByAppendingPathComponent:subpaths[index]];
            //  執行文件剪切操作
            /*
             * 參數一:文件在哪里的全路徑
             * 參數二:文件要被剪切到哪里的全路徑
             */
            [[NSFileManager defaultManager] moveItemAtPath:fromFullpath toPath:toFullpath error:nil];

           NSLog(@"拼接需要復制的文件的全路徑:%@ -- 拼接目標目錄的全路徑:%@ -- 當前線程:%@",fromFullpath,toFullpath,[NSThread currentThread]);
       });

}

執行結果:


GCD迭代截圖

隊列組

  • 假如開發中有多個任務,要求在所有任務都在子線程中并發執行,且不能使用柵欄函數,當所有任務都執行完成后打印“完成”。這樣的需求就需要用到GCD中的隊列組。
  • 應用場合:
    • 對多個任務有強制依賴性,缺一不可時使用

1.隊列組的基本使用

- (void)test10
{
    // 獲取隊列組,用來管理隊列
    dispatch_group_t group = dispatch_group_create();

    //  獲取并發隊列
    dispatch_queue_t queue = dispatch_queue_create("cs", DISPATCH_QUEUE_CONCURRENT);

    //  添加任務
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs1---%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs2---%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs3---%@", [NSThread currentThread]);
    });

    //  攔截通知:當隊列組中所有的任務都執行完畢后,會調用下面方法的block塊
    dispatch_group_notify(group, queue, ^{
        NSLog(@"完成");
    });
}

執行結果:


隊列組截圖

隊列組函數內部操作簡要流程

處理流程:
1.封裝任務
2.把任務提交到隊列
3.把當前任務的執行情況納入到隊列注的監聽范圍

注意:下面方法本身是異步的
dispatch_group_notify(group, queue, ^{

    });

拓展:
在一些框架或者早期項目中,可能會見到下面2種隊列組的使用方法,在這邊順帶提及一下,但不推薦使用,因為太過繁瑣。

第一種

- (void)test11
{
    //  獲得隊列組,管理隊列
    dispatch_group_t group = dispatch_group_create();

    //  獲得并發隊列
    dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);

    //  表示開始把后面的異步任務納入到監聽范圍
    //dispatch_group_enter & dispatch_group_leave
    dispatch_group_enter(group);

    //  使用異步函數封裝任務
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);

        //  通知隊列組該任務已經執行完畢
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    //  攔截通知
    dispatch_group_notify(group, queue, ^{
        NSLog(@"--完成---");
    });
}

第二種


- (void)test11
{
    //  獲得隊列組,管理隊列
    dispatch_group_t group = dispatch_group_create();

    //  獲得并發隊列
    dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);

    //  表示開始把后面的異步任務納入到監聽范圍
    //dispatch_group_enter & dispatch_group_leave
    dispatch_group_enter(group);

    //  使用異步函數封裝任務
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);

        //  通知隊列組該任務已經執行完畢
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    //  等待DISPATCH_TIME_FOREVER 死等,一直要等到所有的任務都執行完畢之后才會繼續往下執行
    //  同步執行
    dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 0.00001 * NSEC_PER_SEC);

    //  等待timer m的時間 不管隊列中的任務有沒有執行完畢都繼續往下執行,如果在該時間內所有事任務都執行完畢了那么會返回一個0,否則是非0值
    long n =  dispatch_group_wait(group, timer);
    NSLog(@"%ld",n);

    NSLog(@"--完成---");
}


補充:同步\異步函數另一種創建方式

  • 其實同步函數和異步函數還有另外的創建方式,但是使用起來比較不方便,所以上面就沒提及,想想還是補充一下好了

1.異步函數(創建一個使用函數封裝代碼的異步函數)

- (void)test12
{
    /**
     *  參數一:隊列
     *  參數二:要傳給函數的參數
     *  參數三:函數
     */
    dispatch_async_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}

void testTask(void *param)
{
    NSLog(@"%@", [NSThread currentThread]);
}

2.同步函數(創建一個使用函數封裝代碼的同步函數)

- (void)test12
{
    /**
     *  參數一:隊列
     *  參數二:要傳給函數的參數
     *  參數三:函數
     */
    dispatch_sync_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}

void testTask(void *param)
{
    NSLog(@"%@", [NSThread currentThread]);
}

上面使用的是函數來封裝要處理的代碼,使用比較不方便,且block是輕量級的數據結構,更推薦使用block封裝代碼的形式創建同步\異步函數。

GCD一些需要注意的細節

  • 全局并發隊列是默認存在的(在我們程序運行的時候就存在)
  • 全局隊列根據隊列的優先級分為 (高,默認,低,后臺優先級)4個并發隊列
  • iOS 6之前,我們通過創建的線程,是要自己手動施放的
    • 施放的方式 —— dispatch_release()
  • 使用柵欄函數,蘋果官方文檔明確規定柵欄函數只有在和使用create函數創建的筆法隊列一起使用才有效
  • 暫時就想到這么多O(∩_∩)O,因為GCD已經開源,想研究的朋友可以到網上搜索一下,有哪里不對的可以聯系我,謝謝!

NSOperation簡單使用

NSOperation作用

  • 配合使用NSOperation和NSOperationQueue也能實現多線程編程

NSOperation和NSOperationQueue實現多線程的具體步驟

  • 先將需要執行的操作封裝到一個NSOperation對象中
  • 然后將NSOperation對象添加到NSOperation對象中
  • 系統會自動將NSOperationQueue中的NSOperation取出來
  • 將取出來的NSOperation封裝的操作放到一條新線程中執行

NSOperation的子類

  • NSOperation是個抽象類,并不具備封裝操作的能力,必須使用它的子類
  • 使用NSOperation子類的方式有3種
    • NSInvocationOperation
    • NSBlockOperation
    • 自定義子類繼承NSOperation,實現內部相應的方法

NSOperation封裝操作

  • 第一種方式 —— NSInvocationOperation
- (void)invocationTest
{
    /**
     * 參數一:目標對象
     * 參數二:調用方法
     */
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    
    //  開啟任務
    [op1 start];
}

- (void)download
{
    NSLog(@"下載:%@",[NSThread currentThread]);
}

執行結果:需要和隊列并用才會開啟子線程執行任務


NSInvocationOperation
  • 第二種方式 —— Block
- (void)blockTest
{
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];
    
    [op1 addExecutionBlock:^{
        NSLog(@"增加的下載:%@", [NSThread currentThread]);
    }];
    
    //  開啟任務
    [op1 start];
    [op2 start];
    [op3 start];
}

- (void)download
{
    NSLog(@"下載:%@",[NSThread currentThread]);
}

執行結果:如果一條線程中執行的操作大于1就會開啟新線程并發執行


NSBlockOperation
  • 方式三 —— 自定義NSOperation

1.先創建一個繼承自NSOperation的類并重寫main方法


- (void)main
{
    NSLog(@"當前線程:%@", [NSThread currentThread]);
}

2.在需要使用的類中引用自定義的類,并創建開啟任務

- (void)custom
{
    SJOperation *op1 = [[SJOperation alloc] init];
    
    [op1 start];
}

執行結果:需要手動開啟線程或者與隊列并用才會開啟子線程


自定義NSOperation

NSOperation中的隊列

  • 主隊列 (獲取方式:+mainQueue)
    • 所有在主隊列中的任務都在主線程中執行
    • 本質上是串行隊列
- (void)invocationQueue
{
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
    //  獲取主隊列
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    
}

執行結果:所有任務都在主隊列中執行,且是串行隊列

NSInvocationOperation在主隊列使用情況
  • 非主隊列(獲取方式:alloc init)
    • 同時具備并發和串行功能
    • 默認下是并發的
- (void)invocationQueue
{
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
    //  獲取非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    
}

執行結果:所有任務在子線程中并發執行

NSInvocationOperation在非主隊列使用情況

注意:addOperation:內部已經幫我們執行了開啟任務方法,所有不需要另外實現。


NSBlockOperation與隊列并用的簡單寫法

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載:%@",[NSThread currentThread]);
    }];

執行結果:所有任務都在子線程中并發執行

NSBlockOperation在隊列中的簡單寫法

設置最大并發數

  • 在NSOperation中,我們要想控制串行隊列或者并發隊列,只需要設置maxConcurrentOperationCount屬性即可
    • 一般我們要使用串行隊列,只需設置值為1即可
    • 如果值大于1,則為并發隊列

1.串行隊列示例

- (void)blockQueue
{
    //  創建非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //  設置最大并發數為1,則隊列為串行隊列
    queue.maxConcurrentOperationCount = 1;
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載1:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載2:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載3:%@",[NSThread currentThread]);
    }];
    
}

執行結果:按照任務添加順序執行,所以是串行隊列


NSOperationQueue串行執行

2.并發隊列示例

- (void)blockQueue
{
    //  創建非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //  設置最大并發數為6,一般子線程控制在6以內,太多線程會使設備壓力過大
    queue.maxConcurrentOperationCount = 6;
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載1:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載2:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下載3:%@",[NSThread currentThread]);
    }];
    
}

執行結果:程序并沒有按照添加順序完成任務,所以是并發執行

NSOperationQueue并發執行

注意:

  1. 一般子線程控制在6以內,太多線程會使設備壓力過大
  2. maxConcurrentOperationCount默認值為-1(在計算機中,-1一般指最大值)
  3. 如果將maxConcurrentOperationCount設置為0,說明同一時間內執行0個任務,所以任務將不會執行。
maxConcurrentOperationCount系統默認值

NSOperation暫停、恢復和取消功能

  • 在NSOperation中,已經為我們提供了暫停、恢復和取消的功能,我們只需調用相應的方法即可。

1.暫停

    //  暫停
    [queue setSuspended:YES];

2.恢復

    //  取消
    [queue setSuspended:NO];

3.取消

    //  取消隊列中所有操作,且取消后的任務不可恢復
    [queue cancelAllOperations];

注意:

1.隊列中的的任務是有狀態的,分別是 —— 等待;執行;完成三種狀態,且暫停、恢復和取消操作并不能作用于當前正處于執行狀態的任務,只能作用于等待狀態的任務。
2.如果是自定義的NSOperation,會發現暫停、恢復操作對其無效,對于這種情況,可以用以下方式解決 —— 使用取消操作

- (void)main
{
    //  模擬耗時操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"1當前線程:%@", [NSThread currentThread]);
    }
    //  判斷當前狀態,如果已經取消,直接返回
    if (self.cancelled) return;
    
    //  模擬耗時操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"2當前線程:%@", [NSThread currentThread]);
    }
    //  判斷當前狀態,如果已經取消,直接返回
    if (self.cancelled) return;
    
    //  模擬耗時操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"3當前線程:%@", [NSThread currentThread]);
    }
    //  判斷當前狀態,如果已經取消,直接返回
    if (self.cancelled) return;
}

解決問題思路:其實這是蘋果官方文檔中的建議 —— 因為,當我們調用cancelAllOperations:方法的時候,他內部的cancelled屬性就會為真,每執行完一個耗時操作后都進行一次判斷,如果發現已經取消,則退出執行。如果想更精確操控的話,也可以將判斷操作放到耗時操作中,但是不建議這樣做,因為這樣性能極差。


NSOperation中的依賴操作

  • NSOperation提供了一套非常便捷好用的操作依賴方式,比起GCD,那種酸爽簡直不敢相信
- (void)blockQueue
{
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載1:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載2:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載3:%@",[NSThread currentThread]);
    }];
    
    //  設置依賴關系
    //  op1依賴op2,只有當op2執行完畢后,才會執行op1
    [op1 addDependency:op2];
    //  op2依賴op3,只有當op3執行完畢后,才會執行op2
    [op2 addDependency:op3];
    
    //  獲取主隊列
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];

執行結果:先執行完op3,等op3執行完成后才執行op2,當op2執行完畢后,才執行op1

NSOperation操作依賴

注意

  • NSOperation提供的操作依賴功能特別強大,可以設置不同隊列的依賴
  • 但是不能循環依賴,比如op1依賴op2,op2又依賴op1,而且并不會報錯,但會發生死鎖,且有關任務都不執行。

NSOperation的監聽

  • 我們經常有這樣的需要:在某些任務執行完成后,再執行指定的某些操作,那么NSOperation中的監聽功能就派上用場了,使用非常簡單
    NSOperation *op = [[NSOperation alloc] init];
    
    op.completionBlock = ^{
        NSLog(@"下載完成");
    };

    [op start];

NSOperation線程間通信

  • NSOperation線程間的通信類似于GCD,所以就不多敘述了,直接上代碼
- (void)downloadPhoto
{
    //  獲取非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    //  創建下載任務
    [queue addOperationWithBlock:^{
       
        //  圖片地址
        NSURL *url = [NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201512/05/20151205092106_aksZU.jpeg"];
        //  下載圖片
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        //  轉換圖片
        UIImage *image = [UIImage imageWithData:imageData];
        //  回到主線程刷新
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            
            NSLog(@"回%@線程刷新UI", [NSThread currentThread]);
            
            self.imageView.image = image;
        }];
        
    }];
}

執行結果:

NSOperation線程間通信.gif

GCD和NSOperation區別

在開發中最常用的就是GCD和NSOperation來進行多線程開發,NSThread更多是在測試時輔助使用,pthread則很少看見,這里為大家簡單整理一下他們之間的區別

  • GCD和NSOperation的對比

    • GCD是純C語言的API,而操作隊列(NSOperation)則是Object-C的對象
    • 在GCD中,任務用Block塊來表示,而塊是輕量級的數據結構,相反,操作隊列(NSOperation)中的操作NSOperation是比較重量級的Object-C對象
  • 那么在開發中如何選擇呢?

    • 一般如果任務之間有依賴關系或者需要監聽任務執行的過程(KVO),首選NSOperation
    • 單純進行一些耗時操作則選用GCD,因為相比NSOperation,GCD效率更高,性能更好
  • NSOperation和NSOperationQueue好處

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

推薦閱讀更多精彩內容