iOS中的多线程编程:重温GCD(二)

引言

此文是iOS中的多线程编程:重温GCD(一)系列的第二部分。将会辅以一定的例子简单讲解一些更深层次的API使用及注意事项。

ps:为了更好的阅读体验,推荐戳我的个人博客:objc.in来观看~博客也会第一时间更新在个人博客上而不是简书上。

栅栏 | Using Barriers

使用barrier构建一个安全的读写操作

上篇文章中,我们最后提到了如何利用GCD创建线程安全的单例。但这其实是远远不够的。考虑这样一个问题:

如果我们的单粒中有这样的可变属性

@property (nonatomic, copy) NSMutableArray *array;`

我们了解,在objc中,apple明确告诉我们这样的可变集合都不是线程安全的。这意味着,如果我们的单粒在多个线程中被读写,很容易就发生数据混乱的问题!

dispatch barrier可以很好的解决这个问题!

根据apple官方文档:

A dispatch barrier allows you to create a synchronization point within a concurrent dispatch queue. When it encounters a barrier, a concurrent queue delays the execution of the barrier block (or any further blocks) until all blocks submitted before the barrier finish executing. At that point, the barrier block executes by itself. Upon completion, the queue resumes its normal execution behavior.

简单翻译下,可见dispatch barrier允许我们在一个并发队列中创建一个同步点,你也可以把它理解为一个任务(block),当队列中的任务按序派发到这里时,并发队列会停下等待barrier点前面的所有任务执行完毕,接着执行barrier block。等barrier block执行完毕后继续执行后面的任务:

Dispatch-Barrier-Swift-480x272.png

试想一下,如果我们在两个线程同时在操作这个数组,如果两个线程都在同时读取,那不会引起问题。如果一个线程中在读取,同时另外一个线程中在写入,就会引起线程不安全的问题!

所以我们应该不把这个属性暴露在外部,使得外部不能直接写入,而是提供给外部一些方法:

- (void)add:(id)object;
- (void)remove:(id)object;

然后,我们在内部利用barrier进行安全的写入:

- (void)add:(id)object{
    if(!object) return;
    //保证写入时,不会被队列中的异步操作"打扰"
    dispatch_barrier_async(self.concurrentQueue, ^{ 
    [self.array addObject:object];
    
    });
}

这样我们就保证了在操作单例中数组的过程中,不会发生任何异步行为,也就保证了线程安全!

另外需要注意得地方是,我们在上述代码段里使用了self.concurrentQueue为什么我们会使用self.concurrentQueue?

首先要说明的是,self.concurrentQueue是一个自定义的并发队列,它的创建方式:

self.concurrentQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
                                                    DISPATCH_QUEUE_CONCURRENT); 

我们会将单粒中的读操作也放在这个队列里:

- (NSArray *) array
{
    __block NSArray *array; 
    dispatch_sync(self.concurrentQueue, ^{ 
        array = [NSArray arrayWithArray:_photosArray]; 
    });
    return array;
}

这样,才能保证在写操作时候拦住前面的读操作,因为它们都在用一个队列中。当然,这个地方必须使用dispathc_sync函数,如果异步调用意味着函数不会立即返回。那么在外部使用的得到的数组时就可能会出问题,比如:

- (NSMutableArray *)array{
    
     __block NSMutableArray *tmpArray = nil;
    
    dispatch_async(self.concurrentQueue, ^{
        tmpArray = [_array copy];//注意这个地方可能在返回之前还没有调用!
    });
    
    return tmpArray;//tmpArray可能为nil!
}

barrierAPI

barrier是个很有用的特性,在apple的官方文档中,有列出了下列函数供我们使用:

void dispatch_barrier_async( dispatch_queue_t queue, dispatch_block_t block);

void dispatch_barrier_async_f( dispatch_queue_t queue, void* context,dispatch_function_t work);

void dispatch_barrier_sync( dispatch_queue_t queue, dispatch_block_t block);

void dispatch_barrier_sync_f( dispatch_queue_t queue, void* context, dispatch_function_t work);

其实还是很有规律的😄:asyncsync相对表示同步or异步、async_fsync相对表示执行的是block或者dispatch_function_t对象。

由于篇幅有限,在这里就不在解释了。只给出结论:

  1. 无论是dispatch_barrier_syncor dispatch_barrier_async函数,最终都会调用到dispatch_barrier_sync_fordispatch_barrier_async_f

  2. dispatch_barrier_sync_f函数中的void* context参数其实就是我们传给dispatch_barrier_sync函数中的block对象。而dispatch_function_t work参数是block对象里的函数指针(懂的人自然懂😄)。

barrier使用建议

要理解barrier拦住的是队列,也就是说,barrier针对的队列。所以不难给出以下建议:

  1. 不要在自定义串行队列中使用:一个很坏的选择,障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。
  2. 不要在全局并发队列中使用:要小心,这可能不是最好的主意,因为其它系统可能在使用队列而且你不能垄断它们只为你自己的目的。
  3. 最好在自定义并发队列中使用:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。

barrier部分小结

如果想要了解更多关于barrier的实现细节,可以自己下载GCD源码阅读:libdispatch

调度组 | Dispatch Groups

使用Dispatch Groups通知所有任务已经完成

与上文一样,我们会以一个例子开始来介绍Dispatch Groups的部分:

假设你要从网上下载许多张图片,完成之后把它们组合在一起构成新的图像。也就是说你必须把所有图片下载完成后再统一显示:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    void (^block1)() = ^{
        //download img_1 from network...
    
    };
    void (^block2)() = ^{
        //download img_2 from network...

    };
    void (^block3)() = ^{
        //download img_2 from network...

    };

    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block1);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block2);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block3);
    
    //拼接图片
    [self finishImg];

}

这样写显然是有问题的!block1,block2,block3的运行由于是在后台,我们无法确保拼接图片时所有图片已经下载完成。对于这种情况,Dispatch Groups就是个不错的选择:

方法一:使用dispatch_group_notify

- (void)viewDidLoad{
    [super viewDidLoad];
    
    void (^block1)() = ^{
        //download img1 from network...
        NSLog(@"block1 finish");
    };
    
    void (^block2)() = ^{
        //download img2 from network...
        NSLog(@"block2 finish");
   };
    
    void (^block3)() = ^{
        //download img3 from network...
        NSLog(@"block3 finish");
    };
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    dispatch_group_t downloadGroup = dispatch_group_create();
    
    dispatch_group_async(downloadGroup, queue, block1);
    dispatch_group_async(downloadGroup, queue, block2);
    dispatch_group_async(downloadGroup, queue, block3);
    
    dispatch_group_notify(downloadGroup, queue, ^{
        [self finishImg];
    });
}

- (void)finishImg{
    NSLog(@"finishi..");
}

这样,finishImg方法就会在三个block全部执行完毕后才被调用:

2016-08-27 15:12:37.885 总结测试[91285:7916599] block3 finish
2016-08-27 15:12:37.885 总结测试[91285:7916593] block2 finish
2016-08-27 15:12:37.885 总结测试[91285:7916604] block1 finish
2016-08-27 15:12:37.886 总结测试[91285:7916604] finishi..

dispatch_group_notify函数非常灵活,它允许你在group内的任务全部完成后传递一个block作为回调。在上述的方法里,我们将图片拼接方法作为回调。

除了dispatch_group_notify函数,还有个dispatch_group_t字眼你可能会感到陌生,在苹果的官方文档中:

A group of block objects submitted to a queue for asynchronous invocation.

Declaration

typedef struct dispatch_group_s *dispatch_group_t;

Discussion

A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.

Note that the blocks in a group may be run on different queues, and each individual block can add more blocks to the group.

The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.

可见group是异步block的集合。而dispatch group是一种监听这种集合的机制。苹果允许我们在同步或者异步的监听集合里的block完成的情况,而且需要注意到是,group内的block可能是在任何队列里的。就像我们上面的代码中写的那样:

dispatch_group_async(downloadGroup, queue, block1);
dispatch_group_async(downloadGroup, queue, block2);
dispatch_group_async(downloadGroup, queue, block3);

我们可以提交group内的任务到任何queue内。

顺带一提的是,gcd会引用group对象直到任务都完成。

方法二:使用dispatch_group_wait

dispatch_group_wait函数就像它的名字所叙述的那样:等待

long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

dispatch_group_wait函数会等待直到指定的group内的任务全部完成或者超时,从上方的申明我们可以看出,dispatch_group_waitdispatch_group_wait函数一样都需要一个group参数,但是多了一个timeout替代block回调。同时,它还会有一个long型返回值。这个返回时标志了当前任务的执行情况或者是否超时:

dispatch_group_wait 会一直等待,直到任务全部完成或者超时。如果在所有任务完成前超时了,该函数会返回一个非零值。你可以对此返回值做条件判断以确定是否超出等待周期;当然,你可以在这里用 DISPATCH_TIME_FOREVER 让它永远等待。它的意思,勿庸置疑就是,永-远-等-待!于是我们只需要判断返回值是否为0就可以知道当前任务是否完成了。

注意,一定要理解好等待。这意味着dispatch_group_wait阻塞当前线程!另外,如果你不使用DISPATCH_TIME_FOREVER参数而是用DISPATCH_TIME_NOW,该函数会立即返回一个值给你,含义与DISPATCH_TIME_FOREVER一样。当然了,这样的话就不会阻塞当前线程了。但是需要像主线程里的NSRunLoop那样不停循环检测当前任务是否全部完成。

了解了dispatch_group_wait函数的含义,我们不难将第一种方法里的代码改写为:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor grayColor];
    
    void (^block1)() = ^{
        //download img1 from network...
        for (int i = 0 ; i < 500;  i ++) {
            NSLog(@"block1 finish  %d",i);
        }
    };
    
    void (^block2)() = ^{
        //download img2 from network...
        for (int i = 0 ; i < 500;  i ++) {
            NSLog(@"block2 finish  %d",i);
        }

    };
    
    void (^block3)() = ^{
        //download img3 from network...
        for (int i = 0 ; i < 500;  i ++) {
            NSLog(@"block3 finish  %d",i);
        }

    };
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    dispatch_group_t downloadGroup = dispatch_group_create();
    
    dispatch_group_async(downloadGroup, queue, block1);
    dispatch_group_async(downloadGroup, queue, block2);
    dispatch_group_async(downloadGroup, queue, block3);
    
    dispatch_async(queue, ^{
        long result =  dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);
        if (result == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self finishImg];
            });
        }
    });
    
}

需要格外注意的是:由于dispatch_group_wait函数会阻塞当前线程,所以我们使用了dispatch_async函数以便能更快的使viewDidLoad方法结束以给予用户更好的体验。

并行运行的for循环:dispatch_apply

dispatch_apply函数会将block按指定的次数提交到指定的队列里去,之后等待所有任务完成后返回:

- (void)viewDidLoad{
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

    dispatch_apply(10, queue, ^(size_t x) {
        NSLog(@"%zu current thead == %@", x , [NSThread currentThread]);
    });
    
    NSLog(@"finishi!");
}

执行结果:


Markdown

有趣的信息很多,我们可以看到每个任务完成的顺序并不一定,而且所在的线程也不一定。(number不为1的话都是子线程)在所有任务都完成后,才会打印finish

这是因为dispatch_apply函数会阻塞住当前线程,这和dispatch_sync是一样的。所以推荐在dispath_async函数中异步地执行dispatch_apply函数。当然了,关于队列的选择上肯定也要是并发队列,否则没有任何意义:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_async(queue, ^{
        dispatch_apply(10, queue, ^(size_t x) {
            NSLog(@"%zu current thead == %@", x , [NSThread currentThread]);
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"finish %@",[NSThread currentThread]);

        });
        
    });

}

另外,在这篇文章中作者也提到了:使用dispatch_apply函数开辟线程来执行任务可能要比for循环代价大得多,所以使用前要三思。

信号量 | Semaphore

有趣的哲学家问题

在了解信号量之前,可以先看看这个有趣的问题:哲学家就餐问题

以下摘自维基百科:

信号量(英语:Semaphore)又称为信号量、旗语,它以一个整数变量,提供信号,以确保在并行计算环境中,不同进程在访问共享资源时,不会发生冲突。是一种不需要使用忙碌等待(busy waiting)的一种方法。

信号量的概念是由荷兰计算机科学家艾兹格·迪杰斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系中,二进制信号量(binary semaphore)又称Mutex。

一个错误的例子

回到我们的代码中,在上一篇iOS中的多线程编程:重温GCD(一)中我曾介绍过:NSMutableArray不是线程安全的,所以以下的写法会有很大的问题:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    NSMutableArray *array  = [NSMutableArray array];
    
    _array = array;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0 ; i < 100;  i ++) {
        dispatch_async(queue, ^{
                [_array addObject:[NSString stringWithFormat:@"%d",i]];
                
        });
    }

}

你可以试试运行上述代码,很容易引发crash。此时就可以使用 Dispatch Semaphore 来避免这个问题。

利用 Dispatch Semaphore 来解决

摘自:《objc高级编程:iOS 与 OS X 多线程与内存管理》

Dispatch Semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,是类似于过马路时常用的手旗,可以通过时举起手旗,不可通过时放下手旗。而在 Dispatch Semaphore 中,使用计数来实现此功能。计数为0时等待,计数为1或者大于1时,减去1而不等待。

创建 dispatch_semaphore_t

上文中我们提到 Dispatch Semaphore 就像是手旗,而dispatch_semaphore_t变量就是那个手旗对象。我们可以这样创建它:

dispatch_semaphore_t semaphore  = dispatch_semaphore_create(1);

其中,参数是long类型:

value

The starting value for the semaphore. Passing a value less than zero causes NULL to be returned.

有了"手旗"🏁,我们就可以用起来啦:

开始使用信号量

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait函数第一个参数天我们创建出来的手旗对象,第二个参数与上文提到的dispatch_group_wait函数中类似,意味着等待到永远。这正是我们要想的!

于是,我们就可以像下面这样解决我们遇到的问题:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    NSMutableArray *array  = [NSMutableArray array];
    
    _array = array;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_semaphore_t semaphore  = dispatch_semaphore_create(1);
    
    for (int i = 0 ; i < 100;  i ++) {
        dispatch_async(queue, ^{
            
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//永远等待信号量 >= 1 只有在信号量>=1时才能进行下一步操作 !
            
            [_array addObject:[NSString stringWithFormat:@"%d",i]];
            
            NSLog(@"finish in %@ currentThread",[NSThread currentThread]);
            
            dispatch_semaphore_signal(semaphore);//处理完后 将信号量+1 避免出现问题
        });
    }
}

当然,Dispatch Semaphore 其实更多时候用来处理更加需要“细粒”化得情形,比如上面这种并发处理线程不安全的数组时,用dispatch_barrier一样可以做到,但是无法像 Dispatch Semaphore 这么细粒化。比如你要并发处理某些事情,但是只需要在特定的情形下才需要线程安全,信号量就是个更好的选择,而不是用barrier

The End

耗时近一周终于写完了这两篇文章,收获非常大。在后续的篇章中我简单介绍下关于GCD的实现,欢迎大家围观😄。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容

  • 背景 担心了两周的我终于轮到去医院做胃镜检查了!去的时候我都想好了最坏的可能(胃癌),之前在网上查的症状都很相似。...
    Dely阅读 9,226评论 21 42
  • 一、多线程简介: 所谓多线程是指一个 进程 -- process(可以理解为系统中正在运行的一个应用程序)中可以开...
    寻形觅影阅读 1,010评论 0 6
  • GCD (Grand Central Dispatch) :iOS4 开始引入,使用更加方便,程序员只需要将任务添...
    池鹏程阅读 1,321评论 0 2
  • 这一天,停下了脚步,坐在十字路口, 吹着北京常有的风,看人来人往,车来车往 一群遛狗的人,对狗的爱胜于对人,...
    张小米_阅读 191评论 0 0
  • 前文再续,书接上一回!在昨天的收盘点评《生蚝说:这是非常短暂的回调》中,我们讲到:“ 由于我们假定是短调,因此...
    果园生蚝阅读 1,099评论 0 1