美文网首页程序员iOS DeveloperiOS 开发
iOS中的多线程编程:重温GCD(二)

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

作者: ZhengLi | 来源:发表于2016-08-28 18:23 被阅读113次

    引言

    此文是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的实现,欢迎大家围观😄。

    相关文章

      网友评论

        本文标题:iOS中的多线程编程:重温GCD(二)

        本文链接:https://www.haomeiwen.com/subject/dtxxettx.html