深刻理解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]);
       });

    });
    打印:

    新开了多个线程,任务并发执行。

    至此,难道能够说,我以前的所说的 并行+同步:不新开线程,任务一个接一个执行 串行+同步:不新开线程,任务一个接一个执行 是错的?不,并无错,只是这种说法,是将任务添加到这种模式的内部。