深度使用 iOS多線程

深度使用 iOS多線程 

  1. GCD的隊列類型
    1. 串行(Serial)隊列
    2. 並行(ConCurrent)隊列 
    3. 主隊列 
    4. 全局隊列
  2. GCD的使用
    1. 隊列組
    2. enter & leavel
    3. 信號量
      1. dispatch_group_t 和 dispatch_semaphore_create 結合使用
      2. dispatch_semaphore_signal
    4. Barrier 異步
    5. 同步任務的作用
  3. GCD和NSOperation的區別
  4. 相關

 

GCG 的隊列類型 

一、串行(Serial)隊列 

 

  • 每次只能執行一個任務

  • 以 先進先出 的方式,順序調度隊列中的任務執行(FIF0)

  • 無論隊列中所指定的執行任務函數是同步還是異步,都會等待前一個任務執行完成後,再調度後面的任務

dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", NULL);

串行隊列,同步執行

dispatch_queue_t queue = dispatch_queue_create("FirstSerialQueue", DISPATCH_QUEUE_SERIAL);
  
    dispatch_sync(queue, ^{
        for (int i = 0; i<5; i++) {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"+++++++%@",[NSThread currentThread]);
        }
    });
    NSLog(@"打印第一個完畢");

    dispatch_sync(queue, ^{
        for (int i = 0; i<5; i++) {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"------%@",[NSThread currentThread]);
        }
    });
    NSLog(@"打印第二個完畢");
 
+++++++<NSThread: 0x1d4076400>{number = 1, name = main}
...
打印第一個完畢
------<NSThread: 0x1d4076400>{number = 1, name = main}
...
打印第二個完畢

串行隊列同步執行,順序執行沒意義

串行隊列,異步執行

NSLog(@"===============");

dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", NULL);

for (int i = 0; i < 5; ++i) {
    dispatch_sync(q, ^{
        NSLog(@"%@ - %d", [NSThread currentThread], i);
    });
}
NSLog(@"-----------------");
===============
-----------------
<NSThread: 0x1d7063b40>{number = 8, name = (null)} - 0
<NSThread: 0x1d7063b40>{number = 8, name = (null)} - 1
<NSThread: 0x1d7063b40>{number = 8, name = (null)} - 2
<NSThread: 0x1d7063b40>{number = 8, name = (null)} - 3
<NSThread: 0x1d7063b40>{number = 8, name = (null)} - 4

串行隊列異步執行,先執行主線程任務,再執行子線程的任務,同類線程按照FIFO順序執行

 

二、並行(ConCurrent)隊列 

 

  • 以 先進先出 的方式,併發調度隊列中的任務執行
  • 如果當前調度的任務是 同步(sync) 執行的, 會等待執行任務完成後,在調度新的任務。
  • 如果當前調度任務是 異步(async) 執行的,同時底層線程池有可用線程資源,會再新的線程調度後續任務執行。

並行隊列,同步執行 <=> 串行隊列,同步執行

NSLog(@"===============");
dispatch_queue_t q = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; ++i) {
        dispatch_sync(q, ^{
            NSLog(@"%@ - %d", [NSThread currentThread], i);
        });
    }
NSLog(@"-----------------");

===============
<NSThread: 0x1d0261580>{number = 1, name = main} - 0
<NSThread: 0x1d0261580>{number = 1, name = main} - 1
<NSThread: 0x1d0261580>{number = 1, name = main} - 2
<NSThread: 0x1d0261580>{number = 1, name = main} - 3
<NSThread: 0x1d0261580>{number = 1, name = main} - 4
-----------------

並行隊列,異步執行

NSLog(@"===============");
dispatch_queue_t q = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; ++i) {
        dispatch_async(q, ^{
            NSLog(@"%@ - %d", [NSThread currentThread], i);
        });
    }
NSLog(@"-----------------");

===============
-----------------
<NSThread: 0x1d0261580>{number = 9, name = (null)} - 0
<NSThread: 0x1d0261580>{number = 9, name = (null)} - 2
<NSThread: 0x1d0261580>{number = 10, name = (null)} - 3
<NSThread: 0x1d0261580>{number = 8, name = (null)} - 1
<NSThread: 0x1d0261580>{number = 11, name = (null)} - 4

 

四、全局隊列 

 

是系統爲了方便程序員開發提供,其工作表現與 併發隊列 一致

全局隊列 和 並行隊列 的區別

  • 全局隊列

    • 沒有名稱
    • 無論 MRC & ARC 都不需要考慮釋放
    • 日常開發中,建議使用""全局隊列"
  • 並行隊列

    • 有名字,和 NSThread 的 name 屬性作用類似
    • 如果在 MRC 開發時,需要使用 dispatch_release(q); 釋放相應的對象
    • dispatch_barrier 必須使用自定義的併發隊列
    • 開發第三方框架時,建議使用併發隊列

全局隊列 異步任務

// 1. 隊列
dispatch_queue_t q = dispatch_get_global_queue(0, 0);

// 2. 執行任務
for (int i = 0; i < 5; ++i) {
    dispatch_async(q, ^{
        NSLog(@"%@ - %d", [NSThread currentThread], i);
    });
}

NSLog(@"=====");

運行效果與併發隊列相同

 

三、主隊列 

 

  • 專門用來在主線程上調度任務的隊列
  • 不會開啓線程
  • 以 先進先出 的方式,在 主線程空閒時 纔會調度隊列中的任務在主線程執行
  • 如果當前主線程正在有任務執行,那麼無論主隊列中當前被添加了什麼任務,都不會被調度

主隊列獲取 

dispatch_queue_t queue = dispatch_get_main_queue();
  • 主隊列是負責在主線程調度任務的
  • 會隨着程序啓動一起創建
  • 主隊列只需要獲取不用創建

主隊列,異步執行 

NSLog(@"-----");
dispatch_queue_t queue = dispatch_get_main_queue();

    for (int i = 0; i < 5; ++i) {
        dispatch_async(queue, ^{
            NSLog(@"%@ - %d", [NSThread currentThread], i);
        });
        NSLog(@"---> %d", i);
    }

NSLog(@"=====");

/// 執行結果
-----
---> 0,1,2,3,4
=====
<NSThread: 0x1d406b940>{number = 1, name = main} - 0,1,2,3,4

在 主線程空閒時纔會調度隊列中的任務在主線程執行

主隊列,同步執行 

    // 1. 隊列
    dispatch_queue_t q = dispatch_get_main_queue();

    NSLog(@"=====");

    // 2. 同步
    dispatch_sync(q, ^{
        NSLog(@"%@", [NSThread currentThread]);
    });

    NSLog(@"-----");

主隊列 和 主線程 相互等待會造成死鎖

  • 並隊列異步(子線程)調度主隊列同步不死鎖
/// 並行隊列
dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);
    
    void (^task)() = ^ {
        NSLog(@"%@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"死?");
        });
    };
    
    dispatch_async(queue, task);

主隊列在 主線程空閒時 纔會調度隊列中的任務在主線程執行

 

GCD的使用 

 

多個接口請求數據,需要監聽接口請求完成,才能進行下一步的操作,我們採用網絡請求是異步的。那麼我們要如何監聽呢?

通常情況下,多個網絡請求同時執行,等所有網絡請求返回後,再進行下一步操作,刷新UI,我們會想到 dispatch_group_asyncdispatch_group_notify 結合使用。

 

隊列組 

 

- (void)group1 {

    // 1. 調度組
    dispatch_group_t group = dispatch_group_create();

    // 2. 隊列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);

    // 3. 將任務添加到隊列和調度組
    dispatch_group_async(group, q, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"任務 1 %@", [NSThread currentThread]);
    });
    dispatch_group_async(group, q, ^{
        NSLog(@"任務 2 %@", [NSThread currentThread]);
    });
    dispatch_group_async(group, q, ^{
        NSLog(@"任務 3 %@", [NSThread currentThread]);
    });

    // 4. 監聽所有任務完成
    dispatch_group_notify(group, q, ^{
        NSLog(@"OVER %@", [NSThread currentThread]);
    });

    // 5. 判斷異步
    NSLog(@"come here");
}

單單使用 dispatch_group_async 和 dispatch_group_notify 當任務1、2、3其中添加使用 dispatch_after 延遲模擬網絡請求,其實是有問題的。

 

enter & leavel 

 

// MARK: - 調度組 2 ///通過enter和leave 多線程並行
- (void)group2 {
    // 1. 調度組
    dispatch_group_t group = dispatch_group_create();

    // 2. 隊列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);

    // dispatch_group_enter & dispatch_group_leave 必須成對出現
    dispatch_group_enter(group);
    dispatch_group_async(group, q, ^{
        NSLog(@"任務 1 %@", [NSThread currentThread]);

        // dispatch_group_leave 必須是 block 的最後一句
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_group_async(group, q, ^{
        NSLog(@"任務 2 %@", [NSThread currentThread]);

        // dispatch_group_leave 必須是 block 的最後一句
        dispatch_group_leave(group);
    });

    // 4. 阻塞式等待調度組中所有任務執行完畢
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    // 5. 判斷異步
    NSLog(@"OVER %@", [NSThread currentThread]);
}

2017-05-18 11:02:24.233  Request_2,<NSThread: 0x60800027ab80>{number = 3, name = (null)}
2017-05-18 11:02:24.233  Request_3,<NSThread: 0x60800027ab80>{number = 3, name = (null)}
2017-05-18 11:02:25.329  Request_1,<NSThread: 0x600000261ac0>{number = 1, name = main}
2017-05-18 11:02:25.330  完成,<NSThread: 0x60800026eb40>{number = 4, name = (null)}

 

信號量 

 

dispatch_semaphore_create   創建一個semaphore
dispatch_semaphore_signal   發送一個信號
dispatch_semaphore_wait    等待信號

簡單的介紹一下這三個函數,第一個函數有一個整形的參數,我們可以理解爲信號的總量,dispatch_semaphore_signal是發送一個信號,自然會讓信號總量加1,dispatch_semaphore_wait等待信號,當信號總量少於0的時候就會一直等待,否則就可以正常的執行,並讓信號總量-1,根據這樣的原理,我們便可以快速的創建一個併發控制來同步任務和有限資源訪問控制。

 

dispatch_group_t 和 dispatch_semaphore_create 結合使用

 

///通過semaphore 多線程並行
- (void)example3 {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_t group = dispatch_group_create();
    
    ///初使值爲0的semaphore
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    
    dispatch_group_async(group, queue, ^{
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
                       
            dispatch_get_main_queue(), ^{
                ///可以理解爲:信號量的值加一
                dispatch_semaphore_signal(sem);
                NSLog(@"Request_1,%@", [NSThread currentThread]);
        });
        ///可以理解爲:信號量的值減一
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    });
    
    dispatch_group_async(group, queue, ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            dispatch_semaphore_signal(sem);
            NSLog(@"Request_2,%@", [NSThread currentThread]);
        });
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"Request_3,%@", [NSThread currentThread]);
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"請求完成,%@", [NSThread currentThread]);
    });
}
Request_2
Request_3
Request_1
任務均完成,刷新界面

相關

停車場剩餘4個車位,那麼即使同時來了四輛車也能停的下。如果此時來了五輛車,那麼就有一輛需要等待。

信號量的值就相當於剩餘車位的數目,dispatch_semaphore_wait函數就相當於來了一輛車,dispatch_semaphore_signal

就相當於走了一輛車。停車位的剩餘數目在初始化的時候就已經指明瞭(dispatch_semaphore_create(long value)),

調用一次dispatch_semaphore_signal,剩餘的車位就增加一個;調用一次dispatch_semaphore_wait剩餘車位就減少一個;

當剩餘車位爲0時,再來車(即調用dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主

沒有耐心,給自己設定了一段等待時間,這段時間內等不到停車位就走了,如果等到了就開進去停車。而有些車主就像把車停在這,

所以就一直等下去。

 

dispatch_semaphore_signal 使用

 

///FIFO
- (void)example5 {
    
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    
    dispatch_queue_t queue = dispatch_queue_create("testBlock", NULL);
    
    dispatch_async(queue, ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"1");
            dispatch_semaphore_signal(sem);
        });
        
    });
    dispatch_async(queue, ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2");
            dispatch_semaphore_signal(sem);
        });
        
    });
    
    dispatch_async(queue, ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"3");
            dispatch_semaphore_signal(sem);
        });
    });
    
    dispatch_async(queue, ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"4");
            dispatch_semaphore_signal(sem);
        });
    });

}

2017-05-18 10:49:57.246  [1441:96225] 1
2017-05-18 10:49:57.247  [1441:96225] 2
2017-05-18 10:49:57.247  [1441:96225] 3
2017-05-18 10:49:57.248  [1441:96225] 4

 

dispatch_semaphore

 

  1. dispatch_semaphore_create
dispatch_semaphore_t  dispatch_semaphore_create(long value);

傳入的參數爲long,輸出一個dispatch_semaphore_t類型且值爲value的信號量。 值得注意的是,這裏的傳入的參數 value >= 0,否則dispatch_semaphore_create會返回NULL。

  1. dispatch_semaphore_wait
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

這個函數會使傳入的信號量dsema的值減1;

這個函數的作用是這樣的,如果dsema信號量的值大於0,該函數所處線程就繼續執行下面的語句,並且將信號量的值減1;如果desema的值爲0,那麼這個函數就阻塞當前線程等待timeout(注意timeout的類型爲dispatch_time_t,不能直接傳入整形或float型數),如果等待的期間desema的值被dispatch_semaphore_signal函數加1了, 且該函數(即dispatch_semaphore_wait)所處線程獲得了信號量,那麼就繼續向下執行並將信號量減1。 如果等待期間沒有獲取到信號量或者信號量的值一直爲0,那麼等到timeout時,其所處線程自動執行其後語句。

  1. dispatch_semaphore_signal

dispatch_semaphore_signal的返回值爲long類型,當返回值爲0時表示當前並沒有線程等待其處理的信號量,其處理 的信號量的值加1即可。當返回值不爲0時,表示其當前有(一個或多個)線程等待其處理的信號量,並且該函數喚醒了一個等待的線程(當線程有優先級時,喚醒優先級最高的線程;否則隨機喚醒)。

dispatch_semaphore_wait的返回值也爲long型。當其返回0時表示在timeout之前,該函數所處的線程被成功喚醒。當其返回不爲0時,表示timeout發生。

  1. timeout

在設置timeout時,比較有用的兩個宏:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER

  • DISPATCH_TIME_NOW  表示當前;

  • DISPATCH_TIME_FOREVER  表示遙遠的未來;

創建 dispatch_time_t 類型的變量有兩種方法,dispatch_time 和 dispatch_walltime。

  • 利用創建 dispatch_time 創建 dispatch_time_t 類型變量的時候一般也會用到這兩個變量。
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);

其參數when需傳入一個dispatch_time_t類型的變量,和一個delta值。表示when加delta時間就是timeout的時間。

///表示當前時間向後延時一秒爲timeout的時間。
例如:dispatch_time_t  t = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);

 

Barrier 異步 

 

  • 主要用於在多個異步操作完成之後,統一對 非線程安全 的對象進行更新
  • 適合於 大規模的 I/O 操作
@interface ViewController () {
    // 加載照片隊列
    dispatch_queue_t _photoQueue;
}

@property (nonatomic, strong) NSMutableArray *photoList;
@end

- (NSMutableArray *)photoList {
    if (_photoList == nil) {
        _photoList = [[NSMutableArray alloc] init];
    }
    return _photoList;
}

操作 photoList 是非線程安全的

- (void)viewDidLoad {
    [super viewDidLoad];

    _photoQueue = dispatch_queue_create("com.itheima.com", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < 20; ++i) {
        [self loadPhotos:i];
    }
}
  • 模擬下載照片並在完成後添加到數組
- (void)loadPhotos:(int)index {

    dispatch_async(_photoQueue, ^{
        [NSThread sleepForTimeInterval:1.0];

        NSString *fileName = [NSString stringWithFormat:@"%02d.jpg", index % 10 + 1];
        NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
        UIImage *image = [UIImage imageWithContentsOfFile:path];

        [self.photoList addObject:image];
        NSLog(@"添加照片 %@", fileName);
    });
}

由於操作 photoList 是非線程安全的,如果出現兩個線程在同一時間向數組中添加對象,會出現程序崩潰的情況

NSLog(@"添加照片 %@", fileName);
dispatch_barrier_async(_photoQueue, ^{
    [self.photoList addObject:image];
    NSLog(@"OK %@", [NSThread currentThread]);

});

使用 dispatch_barrier_async 添加的 block 會在之前添加的 block 全部運行結束之後,纔在同一個線程順序執行,從而保證對非線程安全的對象進行正確的操作!

 

同步任務的作用 

 

同步任務,可以讓其他異步執行的任務,依賴某一個同步任務

例如: 在用戶登錄之後,再異步下載文件!

NSLog(@"1============");
dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_sync(queue, ^{
    NSLog(@"登錄 %@", [NSThread currentThread]);
});
   
dispatch_async(queue, ^{
    NSLog(@"下載 A %@", [NSThread currentThread]);
});
   
dispatch_async(queue, ^{
    NSLog(@"下載 B %@", [NSThread currentThread]);
});
NSLog(@"2============");

1============
<NSThread: 0x1d0071bc0>{number = 1, name = main}
2============
下載 A <NSThread: 0x1d2e71d40>{number = 9, name = (null)}
下載 B <NSThread: 0x1d42780c0>{number = 8, name = (null)}
  • 代碼改造,讓登錄也在異步執行
dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);

void (^task)() = ^{
    dispatch_sync(queue, ^{
        NSLog(@"登錄 %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"下載 A %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"下載 B %@", [NSThread currentThread]);
    });
};

dispatch_async(queue, task);

登錄 <NSThread: 0x1d2475100>{number = 9, name = (null)}
下載 A <NSThread: 0x1d2475100>{number = 9, name = (null)}
下載 B <NSThread: 0x1c4068dc0>{number = 10, name = (null)}

 

GCD和NSOperation的區別 

 

GCD是基於C的api,NSOperation屬於Object-C類,操作隊列是GCD實現Object-C API。

相當於與GCD

  1. NSOperation擁有更多的函數,具體查看api。
  1. 在NSOperationQueue中,可以建立NSOperation之間的依賴的關係。
  1. 配合KVO,可以檢測Operation是否正在執行(isExecuted)、是否結束(isFinished)、是否取消(isCancled)。

4.NSOperation可以管理併發,NSOperation之間的優先級。

相對於NSOperation

GCD主要與Block結合使用,代碼簡潔高效。

GCD也可以實現複雜的多線程應用,只是相對於NSOperation而言要複雜。

具體使用,根據需求而定。

這兩者直接有什麼區別呢?

  1. GCD是底層的C語言構成的API, 而NSOperationQueue及相關對象是objc對象。在GCD中,在隊列中執行的是Block構成的任務,這是一個輕量級的數據結構;而Operation作爲一個對象,爲我們提供了更多的選擇。
  1. 在NSOperationQueue中,我們隨時可以取消已經設定要準備執行的任務,GCD可以,但是需要更多複雜的代碼。
  1. NSOperation能夠方便設置依賴關係,我們可以讓一個Operation依賴於另一個Operation,這樣的話儘管兩個Operation處於同一個並行隊列中,但是前者會直到後者執行完畢後再執行。
  1. 我們能將KVO應用在Operation中,可以監聽一個Operation是否完成或取消,這樣子能比GCD更加有效掌握我們執行的後臺任務。
  1. 在NSOperation中,我們能設置NSOperation的priority優先級,能夠使同一個並行隊列中的任務區分先後地執行,而在GCD中,我們只能區分不同任務隊列的優先級,如果要區分Block任務的優先級,也需要大量的複雜代碼。
  1. 我們能夠對Operation進行繼承,在這之上添加成員變量與成員方法,提高整個代碼的複用度,能夠在其之上添加更多自定製的功能。

總得來說,Operation Queue 提供了更多你在編寫多線程程序時需要的功能,並隱藏了許多線程調度,線程取消與線程優先級的複雜代碼,爲我們提供簡單的API入口。

操作隊列提供了在GCD中不那麼容易複製的有用特性,最重要一個就是可以取消隊列中的任務;而且操作隊列在管理操作間的依賴關係方面也容易一些。另一面GCD賦予你更多的控制權力以及操作隊列中所不能使用的底層函數

 

相關 

 

1、GCD的認識

2、多個網絡請求併發執行、順序執行

3、把握AFNet網絡請求完成的正確時機

4、關於dispatch_semaphore的使用

發佈了40 篇原創文章 · 獲贊 20 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章