深刻理解GCD 第二篇(dispatch_group、dispatch_barrier、基於線程安全的多讀單寫)

iOS開發:深刻理解GCD 第二篇(dispatch_group、dispatch_barrier、基於線程安全的多讀單寫)
Dispatch Group
在追加到Dispatch Queue中的多個任務處理完畢以後想執行結束處理,這種需求會常常出現。若是隻是使用一個Serial Dispatch Queue(串行隊列)時,只要將想執行的處理所有追加到該串行隊列中並在最後追加結束處理便可,可是在使用Concurrent Queue 時,可能會同時使用多個Dispatch Queue時,源代碼就會變得很複雜。

在這種狀況下,就可使用Dispatch Group。
dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.gcd-group.www", DISPATCH_QUEUE_CONCURRENT);
     
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 1000; i++) {
            if (i == 999) {
                NSLog(@"11111111");
            }
        }
         
    });
     
    dispatch_group_async(group, queue, ^{
        NSLog(@"22222222");
    });
     
    dispatch_group_async(group, queue, ^{
        NSLog(@"33333333");
    });
     
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });
控制檯的輸出:


由於向Concurrent Dispatch Queue 追加處理,多個線程並行執行,因此追加處理的執行順序不定。執行順序會發生變化,可是此執行結果的done必定是最後輸出的。

不管向什麼樣的Dispatch Queue中追加處理,使用Dispatch Group均可以監視這些處理執行的結果。一旦檢測到全部處理執行結束,就能夠將結束的處理追加到Dispatch Queue中,這就是使用Dispatch Group的緣由。

下面試一個使用Dispatch Group異步下載兩張圖片,而後合併成一張圖片的medo(注意,咱們老是應該在主線程中更新UI):
#import "ViewController.h"
 
@interface ViewController ()
@property (nonatomic, strong) UIImage *imageOne;
@property (nonatomic, strong) UIImage *imageTwo;
@property (nonatomic, weak) UILabel *textLabel;
@end
 
@implementation ViewController
  • (void)viewDidLoad {面試

    [super viewDidLoad];
        
       [self operation1];

    }數據庫

  • (void)operation1
    {xcode

    UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(200, 450, 0, 0)];
       textLabel.text = @"正在下載圖片";
       [textLabel sizeToFit];
       [self.view addSubview:textLabel];
       self.textLabel = textLabel;
       [self group];
       NSLog(@"在下載圖片的時候,主線程貌似還能夠乾點什麼");

    }安全

  • (void)group
    {多線程

    UIImageView *imageView = [[UIImageView alloc] init];
       [self.view addSubview:imageView];
        
       dispatch_group_t group = dispatch_group_create();
       dispatch_queue_t queue = dispatch_queue_create("cn.gcd-group.www", DISPATCH_QUEUE_CONCURRENT);
        
       dispatch_group_async(group, queue, ^{
           NSLog(@"正在下載第一張圖片");
           NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912213125372-589808688.png"]];
           NSLog(@"第一張圖片下載完畢");
           self.imageOne = [UIImage imageWithData:data];
       });
        
       dispatch_group_async(group, queue, ^{
           NSLog(@"正在下載第二張圖片");
           NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912212457684-585830854.png"]];
           NSLog(@"第二張圖片下載完畢");
           self.imageTwo = [UIImage imageWithData:data];
       });
        
       dispatch_group_notify(group, queue, ^{
           UIGraphicsBeginImageContext(CGSizeMake(300, 400));
            
           [self.imageOne drawInRect:CGRectMake(0, 0, 150, 400)];
           [self.imageTwo drawInRect:CGRectMake(150, 0, 150, 400)];
            
           UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
           UIGraphicsEndImageContext();
            
           dispatch_async(dispatch_get_main_queue(), ^{
               UIImageView *imageView = [[UIImageView alloc] initWithImage:newImage];
               [self.view addSubview:imageView];
               self.textLabel.text = @"圖片合併完畢";
           });
       });

    }
    @end併發

    dispatch_barrier_async
    在訪問數據庫或者文件的時候,咱們可使用Serial Dispatch Queue可避免數據競爭問題,代碼以下所示:
    先看看,若是咱們在日常編碼中,若是要保證某個屬性能夠線程安全的讀寫,如何寫的:異步

    #import <Foundation/Foundation.h>async

    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end性能

    #import "ZYPerson.h"測試

  1. NSString *_name;
    @implementation ZYPerson

  • (void)setName:(NSString *)name
    {

    @synchronized(self) {
           _name = [name copy];
       }

    }

  • (NSString *)name
    {

    @synchronized(self) {
           return _name;
       }

    }
    @end
    這是我在剛學iOS開發,剛涉及併發中的數據競爭時,書本上提到的一種解決方案。若是有多個線程要執行同一份代碼,那麼有時候可能會出現問題,這種狀況下,一般要使用鎖來實現某種同步機制。iOS提供了一種加鎖的方式,就是採用內置的synchronization block,也就是上面代碼所寫的。

    這種寫法會根據給定的對象,自動建立一個鎖,並等待塊中的代碼執行完畢。執行到這段代碼結尾處,鎖也就釋放了。在上面的例子中,同步行爲所針對的對象是self。這麼寫一般沒錯,可是@synchronized(self)會大大下降代碼效率,甚至不少時候,還能夠被人感受到效率明顯降低了,由於共用同一個鎖的那些同步塊,都必須按順序執行。若在self對象上頻繁加鎖,那麼程序可能就要等另外一段與此無關的代碼執行完畢,才能夠繼續執行當前代碼,這樣作是很不必的。

    @synchronized(self)會大大下降代碼效率,由於全部的同步塊( @synchronized(self) )都會彼此搶奪同一個鎖。要是有多個屬性這麼寫,每一個屬性的同步塊( @synchronized(self) )都要等其餘全部的同步塊執行完畢以後才能執行,這並非咱們想要的結果,咱們只想要每一個屬性各自獨立的同步。

    還有,不得不說,按上面這麼作,雖然能夠在必定程度上提供「線程安全」,但卻沒法保證訪問該對象時是絕對線程安全的。事實上,上面的寫法,就是atomic,也就是原子性屬性xcode自動生成的代碼,這種方法,在訪問屬性時,一定能夠從中獲得有效值,然而若是在一個線程上屢次調用getter方法,每次獲得的結果卻未必相同,在兩次讀操做之間,其餘線程可能會寫入新的屬性值。

    其實使用GCD能夠簡單高效的代替同步塊或者鎖對象,可使用,串行同步隊列,將讀操做以及寫操做都安排在同一個隊列裏,便可保證數據同步,代碼以下:

    #import <Foundation/Foundation.h>

    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end

    #import "ZYPerson.h"

    @interface ZYPerson ()
    @end

  1. NSString *_name;

  2. dispatch_queue_t _queue;
    @implementation ZYPerson

  • (instancetype)init
    {

    if (self = [super init]) {
          _queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
       }
       return self;

    }

  • (void)setName:(NSString *)name
    {

    dispatch_sync(_queue, ^{
           _name = [name copy];
       });

    }

  • (NSString *)name
    {

    __block NSString *tempName;
       dispatch_sync(_queue, ^{
           tempName = _name;
       });
       return tempName;

    }
    @end
    這樣寫的思路是:把寫操做與讀操做都安排在同一個同步串行隊列裏面執行,這樣的話,全部針對屬性的訪問操做就都同步了。
    這種方法的確已經足夠好了,但還不是最優的,它只能夠實現單讀、單寫。總體來看,咱們最終要解決的問題是,在寫的過程當中不能被讀,以避免數據不對,可是讀與讀之間並無任何的衝突!

    多個getter方法(也就是讀取)是能夠併發執行的,而getter(讀)與setter(寫)方法是不能併發執行的,利用這個特色,還能寫出更快的代碼來,此次注意,不用串行隊列,而改用並行隊列:

    #import <Foundation/Foundation.h>

    @interface ZYPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end

    #import "ZYPerson.h"

    @interface ZYPerson ()
    @end

  1. NSString *_name;

  2. dispatch_queue_t _concurrentQueue;
    @implementation ZYPerson

  • (instancetype)init
    {

    if (self = [super init]) {
          _concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
       }
       return self;

    }

  • (void)setName:(NSString *)name
    {

    dispatch_barrier_async(_concurrentQueue, ^{
           _name = [name copy];
       });

    }

  • (NSString *)name
    {

    __block NSString *tempName;
       dispatch_sync(_concurrentQueue, ^{
           tempName = _name;
       });
       return tempName;

    }
    @end

    這樣優化,測試一下性能,能夠發現這種作法確定比使用串行隊列要快。

    在這個代碼中,我用了點新的東西,dispatch_barrier_async,能夠翻譯成柵欄(barrier),它能夠往隊列裏面發送任務(塊,也就是block),這個任務有柵欄(barrier)的做用。

    在隊列中,barrier塊必須單獨執行,不能與其餘block並行。這隻對併發隊列有意義,併發隊列若是發現接下來要執行的block是個barrier block,那麼就一直要等到當前全部併發的block都執行完畢,纔會單獨執行這個barrier block代碼塊,等到這個barrier block執行完畢,再繼續正常處理其餘併發block。在上面的代碼中,setter方法中使用了barrier block之後,對象的讀取操做依然是能夠併發執行的,可是寫入操做就必須單獨執行了。

    附錄(也算是追加)

    也許會對並行、串行、異步、同步有所理解不深。
    並行:就是隊列裏面的任務(代碼塊,block)不是一個個執行,而是併發執行,也就是能夠同時執行的意思
    串行:隊列裏面的任務一個接着一個執行,要等前一個任務結束,下一個任務才能夠執行
    異步:具備新開線程的能力
    同步:不具備新開線程的能力,只能在當前線程執行任務

    那麼,若是他們相互串起來,會怎麼樣呢?
    並行+異步:就是真正的併發,新開有有多個線程處理任務,任務併發執行(不按順序執行)
    串行+異步:新開一個線程,任務一個接一個執行,上一個任務處理完畢,下一個任務才能夠被執行
    並行+同步:不新開線程,任務一個接一個執行
    串行+同步:不新開線程,任務一個接一個執行

    如此,並行+異步,串行+異步卻是很好辨別,他們有各自的特色,那麼並行+同步,串行+同步,貌似效果是同樣的,好像用哪一種都同樣??!!
    不,這其中區別很大(不得不說一句,我所看多線程書籍,基本沒講到這點的,不知道是否是他們都認爲這是基礎,不必說明)。

    這其中的區別,我以爲代碼體現更直觀,直接上代碼吧:
    這一份是並行+同步,我在外面併發添加代碼塊:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_main_queue(), ^{
               NSLog(@"11  %@",[NSThread currentThread]);
           });
       });
        
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           dispatch_sync(dispatch_get_main_queue(), ^{
               NSLog(@"22  %@",[NSThread currentThread]);
           });
       });
        
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           dispatch_sync(dispatch_get_main_queue(), ^{
               NSLog(@"33  %@",[NSThread currentThread]);
           });
       });
        
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           dispatch_sync(dispatch_get_main_queue(), ^{
               NSLog(@"44  %@",[NSThread currentThread]);
           });
       });
        
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           dispatch_sync(dispatch_get_main_queue(), ^{
               NSLog(@"55  %@",[NSThread currentThread]);
           });
       });

    看看打印:

    在一個線程裏面,任務一個接一個的執行。

    再看看,並行+同步,併發調用:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           NSLog(@"11  %@",[NSThread currentThread]);
       });

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           NSLog(@"22  %@",[NSThread currentThread]);
       });

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           NSLog(@"33  %@",[NSThread currentThread]);
       });

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           NSLog(@"44  %@",[NSThread currentThread]);
       });

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           NSLog(@"55  %@",[NSThread currentThread]);
       });

    });
    打印:

    新開了多個線程,任務併發執行。

    至此,難道能夠說,我以前的所說的 並行+同步:不新開線程,任務一個接一個執行 串行+同步:不新開線程,任務一個接一個執行 是錯的?不,並無錯,只是這種說法,是將任務添加到這種模式的內部。

相關文章
相關標籤/搜索