workiOS基础知识普及

iOS多线程开发之GCD

2016-04-23  本文已影响243人  我系哆啦

多线程概念

  1. 多线程的缺点
    多线程技术看起来非常美好,但实际上因为涉及到上下文切换,多线程执行的效率未必比单线程快,甚至可能会慢过单线程,而且,多个线程更新相同资源会导致数据的不一致(数据竞争),停止等待事件的线程会导致多个线程相互持续等待(死锁),使用太多线程会消耗掉大量内存等问题.
  2. 多线程的优点
    尽管极易发生各种问题,在iOS中也应当使用多线程编程.因为多线程编程可以保证应用程序的响应性能.
    应用程序在启动时,通过最先执行额线程("主线程")来描绘用户界面,处理屏幕的事件.如果在该线程中进行长时间的处理,如数据库访问,网络请求等,就会妨碍主线程的执行,从而导致不能更新用户界面,应用程序的画面长时间停滞的问题.
    3.为什么要使用多线程
    使用多线程编程,在执行长时间的处理时,仍可保证用户界面的响应性能.这是我们使用多线程编程的最大好处,而且,苹果为了简化多线程的使用,给我们提供了多种多线程技术,本文主要介绍GCD结合NSoperation在开发中的使用.

GCD的API

  1. Dispatch Queue
    Dispatch Queue 是执行处理的等待队列.,应用程序编程人员通过dispatch_async等API,在block中将要执行的处理追加到Dispatch Queue 中,Dispatch Queue 按照追加的顺序(FIFO,先进先出)执行处理.
通过Dispatch Queue 执行处理.png

Dispatch Queue 分为两种类型

上面的代码中,分别创建了serial Dispatch Queue 和Concurrent Dispatch Queue.当为serial Dispatch Queu 时,因为要等待现在执行的处理结束,所以首先执行第一个任务,打印0,然后顺序依次执行其他任务(这里表现为0,1,2,3,4,5,6,7,8,9由小到大按顺序打印).系统只会使用一个线程.当为Concurrent Dispatch Queu 时,因为不用等待现在执行的处理结束,所以首先执行第一个任务,不管第一个任务执行是否结束,都开始执行第二个任务,不管第二个任务执行是否结束,都开始执行第三个任务,如此重复循环.(这里表现为0,1,2,3,4,5,6,7,8,9的打印没有按照有小到大的顺序,是一个随机顺序).系统可以并行执行多个处理,但是并行执行处理数量取决于当前系统的状态,即iOS基于Dispatch Queue 中的处理数,CPU核数以及CPU负荷等当前系统状态来决定Concurrent Dispatch Queue中并行执行的处理数.

Dispatch Queue 队列种类

当生成多个serial Dispatch Queue,各个serial Dispatch Queue 将并行执行,虽然在一个Serial Dispatch queue 中同时只能后执行一个追加处理,但是如果将处理分别追加到4个serial Dispatch queue 中,各个serial Dispatch queue 执行1个,即为同时执行4个处理.如果生成2000个serial Dispatch queue ,那么久生成2000个线程,而不像Concurrent Dispatch queue 那样,系统会根据系统状态来决定执行处理数(生成线程的个数).如果过多使用线程,就会消耗大量内存,引起大量的上下文切换,大幅降低系统的响应性能.因此,只在为了避免多线程变成问题之一---多个线程更新相同资源导致数据竞争时使用serial Dispatch queue.

Dispatch_queue_create 函数生成的Dispatch queue 不管是serial 还是Concurrent,都使用与默认优先级Global Dispatch Queue 相同有限优先级的线程,可以使用Dispatch_set_target_queue函数变更Dispatch queue 的优先级.

2.Dispatch Group
在追加到Dispatch Queue 中的多个处理全部结束后想执行结束处理,开发中经常会碰到这种需求.当只是用一个serial Dispatch queue 的时候,只要将想执行的结果全部追加到serial Dispatch queue 中并在最后追加结束处理即可.但是在使用Concurrent Dispatch queue 或同时使用多个Dispatch queue 是.Dispatch Group就派上用场了.
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"complete");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"blko");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk2");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk3");
});

//打印结果为 blk2 blk1 blko blk3
2016-04-23 15:00:00.260 test01[1687:171508] complete
</pre>

上面这段代码展示了Dispatch Group的用法.Dispatch Group 可以监视追到到Dispatch queue 中的处理的完成情况,一旦监测到所有处理执行结束,就将结束的处理追加到 dispatch_group_notify中指定的Dispatch queue 中执行.

除了使用dispatch_group_async 追加处理到Dispatch queue中,还有另外一函数:Dispatch_group_enter() 和Dispatch_group_leave().

2016.7.11更新:使用Dispatch_group_enter() 和Dispatch_group_leave()可以对网络请求等异步执行线程也执行回调监听

Paste_Image.png

3.dispatch_barrier_async
在访问数据库或文件时,使用serial Dispatch queue 可以避免数据竞争问题.写入处理确实不可与其他的写入处理以及包含读取的其他某些处理并行执行,但是如果读取处理只是与读取处理并行执行,那么多个并行执行处理就不会发生问题.也就是说,为了高效率的访问,读取处理追加到Concurrent Dispatch queue 中,写入处理在任一个读取处理都没有执行的状态下,追加到serial Dispatch queue中即可.用之前的几个接口也可以实现这个功能,但是苹果系统了一个非常方便解决这个问题的接口:dispatch_barrier_async.用代码来演示dispatch_barrier_async的使用.

<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{ NSLog(@"reading1"); });
dispatch_async(queue, ^{ NSLog(@"reading2"); });
dispatch_async(queue, ^{ NSLog(@"reading3"); });
dispatch_async(queue, ^{ NSLog(@"reading4"); });

dispatch_barrier_async(queue, ^{ NSLog(@"writing1"); });

dispatch_async(queue, ^{ NSLog(@"reading5"); });
dispatch_async(queue, ^{ NSLog(@"reading6"); });
dispatch_async(queue, ^{ NSLog(@"reading7"); });
dispatch_async(queue, ^{ NSLog(@"reading8"); });

</pre>

上面的代码打印结果为:2016-04-23 15:21:20.474 test01[1782:184574] reading1
2016-04-23 15:21:20.474 test01[1782:184575] reading2 reading3 reading4
2016-04-23 15:21:20.475 test01[1782:184635] writing1
2016-04-23 15:21:20.475 test01[1782:184574] reading5 reading6 reading7 reading8

Dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue 上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue 中,然后等待由Dispatch_barrier_async 追加的处理结束后,Concurrent Dispatch Queue才恢复为一般的动作.用下图来表示更加明了.将Concurrent Dispatch Queue分为三段.使用Concurrent Dispatch Queue 和Dispatch_barrier_async可以实现高效的函数库访问和文件访问.

Dispatch_barrier_async函数处理流程

4.dispatch_sync
Dispatch_async 函数的async意味着非同步,就是将指定的Block非同步的追加到指定的Dispatch queue中,Dispatch_async函数不做任何等待.
Dispatch_sync 函数的sync意味着同步,就是将指定的Block同步的追加到指定的Dispatch queue中,在追加的Block结束前,Dispatch_sync函数会一直等待.

等待意味着当前线程停止,开发中一定要非常注意这种情况(容易引起死锁).Dispatch_sync其实可以看做简易的Dispatch_group_wait函数.一旦调用Dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回.

<pre>
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"Hello 1");
dispatch_sync(queue, ^{
NSLog(@"Hello 2");
});
});

dispatch_sync(queue, ^{
    NSLog(@"Hello 1");
});

</pre>

分析以上代码,main Dispatch Queue 中执行的Block 等待 main Dispatch Queue中要执行的Block
执行结束.引起死锁.

5.Dispatch Semaphore
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时程序还会异常结束.虽然使用serial Dispatch queue和Dispatch_barrier_async 函数可以避免这类问题,但是当需要进行更细粒度的排他控制时.我们就需要用到Dispatch semaphone了.
Dispatch semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号.在Dispatch Semaphore中.使用计数类实现该功能,计数为0时等待,计数为1或者大于1时,减去1而不等待.比较两段代码:

该代码使用global Dispatch queue 更新NSMutableArray,所以执行后,有内存错误导致程序异常结束的概率很高.

也可以参考同步块(synchronization block) 和NSLock的使用.

5.Dispatch_after
有时候会有这种情况,想在指定的时间后执行处理.这时候可以考虑使用Dispatch_after.需要注意的是,Dispatch_after并不是在指定的时间后执行处理,而只是在指定的时间追加处理到Dispatch queue.因为Mian Dispatch queue在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,Block最快3秒后执行,最慢在3+1/60后执行.虽然在有严格时间的要求下使用Dispatch_after会出问题,但在想大致延迟执行处理时可以使用.

6.Dispatch_once
使用Dispatch_once来执行只需要运行一次的线程安全代码,即单例模式.常用的写法如下:
<pre>

NSOperationQueue

GCD技术确实非常棒,然而还有一种技术"NSOperationQueue",在某些情况下,使用NSOperationQueue比GCD更方便,我们也应该熟悉了解.

在多线程开发中,我们可以结合GCD和NSOperation,来更高效的实现多线程编程.

上一篇下一篇

猜你喜欢

热点阅读