您的位置:首页 > 移动开发 > IOS开发

iOS 多线程使用总结(很实用)

2015-06-19 11:02 302 查看
每次准备开始新的航行,总是要复习一遍算法啊,多线程啊,内存管理啊之类的理论和应用知识,这次把他们整理成文档,方便以后的学习和不断的积累进步。

多线程给我留下的是痛苦的记忆,当时在上家创业公司的最后阶段,就是被Feature Phone上面的多线程方案导致bug丛生,搞的焦头烂额。

(一). 多线程的应用

由于一直做手机应用程序的开发,所以我接触到的多线程方案很少,总结下来只用来解决2类问题:

问题1. 同步问题

问题2. 耗时计算问题

所谓的同步问题是指,比如在多媒体实时系统中,解码模块与播放模块之间的关系,解码模块解码完毕,播放模块就需要进行绘制或者播放声音。一种方案是异步方案,解码完毕通知播放模块播放,或者轮询方案,解码后将数据放入缓冲区,播放模块轮询(用timer机制),这些方案的实时性都无法保证,因此如果对实时性要求较高的应用,用线程同步机制是最好的方案:解码器解码,解锁,播放器播放,继续等待数据。这就是多线程模型中的最基本模型:生产者消费者模型。[详情见2.1]

而耗时的计算问题则比较普遍,尤其是手机上的图片应用,比如图片浏览的应用,当用户快速拖动scroll view的时候,如果加载图片文件,解码,绘制整个过程的时间超过16ms(60fps的要求),就会有卡顿,不够流畅,这时候就需要用后台线程做这些加载文件,解码等费时的操作。

(二). iOS系统提供的多线程方案

iOS系统提供了n多的线程方案,从最底层POSIX库支持,到NSThread,再到Operation Queue,最后到最方便的GCD。

1. iOS支持POSIX线程库,以基本的生产者消费者模型为例(见[参考代码:https://github.com/zteshadow/threadStudy]里面的POSIX工程,所有的代码都是iOS版本的实现,在XCode5.0上面编译),实现这个模型要注意需要3个lock,一个是用来保护对缓冲区的读写操作的,一个是当缓冲区已满的时候锁住生产者,一个是当缓冲区为空的时候锁住消费者。注意iOS不支持匿名锁,所以需要使用sem_open来创建锁。POSIX线程pthread1995年发布,60个函数.

创建:pthread_create

终止:

自己返回:隐式终止

自己调用pthread_exit:显示终止

被别人调用pthread_cancel:被终止

回收线程资源:

pthread_join:阻塞等待线程退出,回收资源:栈,控制结构等

分离和结合:

默认创建的线程是可结合的,由创建者回收资源,可以调用pthread_detach来分离线程,让系统回收资源。

2. NSThread只是比pthread高一层的objective-c抽象,用起来比较复杂,可能还需要维护一个runloop,runloop的源等等,如果为了解决问题2,是完全没有必要使用NSThread的。

NSThread alloc ,start:可以先建立,再配置,再执行

NSThread detachNewThreadSelector:立即开始执行

3. Operation Queue

示例代码演示了一个场景:将一个耗时操作(耗时5秒钟)发送给主线程之外的线程来执行(见[参考代码:https://github.com/zteshadow/threadStudy]里面的NSOperationQueue工程),该工程里在controller的构造函数里面创建了operation
queue,在按钮的响应函数里面创建operation,然后发送给queue去处理,如果设置queue的maxConcurrentOperationCount大于1,则会由系统线程池中的多个线程从queue里面取出operation并发执行,否则顺序执行,Operation Queue是iOS2引入的。

4. GCD

解决上面3同样的问题,可以使用GCD,见[参考代码:https://github.com/zteshadow/threadStudy]里面的GCD工程。可以看到只要把创建NSInvocationOperation,再add到queue换成dispatch_async就可以了,要将现有的基于Operation
Queue的代码改成GCD代码,非常简单——不过似乎也没有什么理由这么做。如果不是迁移Operation Queue的代码,而是新建的代码,那么基于GCD要比Operation Queue简单。dispatch_get_global_queue是个并发执行的queue,如果要顺序执行,需要自己用dispatch_queue_create创建queue(iOS4引入的2010年)。

GCD中的一些知识点:

dispatch_barrier_async工作机制是怎样的?如何实现?

queue中的其他block已经开始在各自分配的线程执行,当从queue检出一个barrier时,是等待其他的block都执行完毕再执行barrier,此时不会并发执行其他的block,直到该barrier执行完毕。 这相当于给当前已经运行的bock们加了一个group和notify,在notify里面执行了barrier,然后再执行barrier下面的block——实际上GCD的实现中的确如此。

dispatch_queue_create 默认是serial的queue,可以用参数:DISPATCH_QUEUE_CONCURRENT,创建并发queue,串行queue——每个对应会创建一个thread,如果持续的增加串行queue,比如说10000个串行queue,那么系统就要生成10000个线程来进行调度,这会严重影响系统性能,甚至导致崩溃,而并发queue,随着dispatch的block的增加,系统会根据当前的core的数量以及当前CPU的usage等综合资源,选择创建新的thread或者是用现有的thread进行调度。

(三).线程间的通信

1. 上面的3和4中,主线程将task发送给系统中的辅助线程执行,当执行完毕如果需要通知主线程的,可以使用:performSelectorOnMainThread,或者使用GCD中的dispatch_async让block在dispatch_get_main_queue上运行。

2. 如果自己创建了NSThread,那么可能需要讲一下runloop。顾名思义,runloop就是一个循环,如果没有runloop,那么我们创建的线程一般需要自己做一个循环来等待事件的发生,类似于这样:

while (wait_for_event)

{

do_some_thing

}

在没有事件的时候阻塞,有事件处理事件。iOS为我们提供了一个runloop代替这个自己做的循环,我们可以调用runloop的run进入循环。就像我们自己创建循环一样,runloop也需要一个等待的事件,如果没有等待事件(runloop的源),那么runloop就会直接返回,然后线程结束了。runloop的源可以是个timer源,也可以是事件源,我们可以自己定义源,也可以使用message port作为输入源。NSPort有3种类型:NSMachPort,NSMessagePort和NSSocketPort,文档中不建议使用NSMessagePort,而且好像在iOS7中cocoa去掉了对NSPortMessage的支持,原因不详。这样就不能用cocoa中的message
port了,可以使用core foundation中的message port,不过稍微麻烦一些见[参考代码:https://github.com/zteshadow/threadStudy]中的CFMessagePortRef工程。注意外部源用signal来发出信号,这个和message port稍有不同,signal只有0和1的状态,而message
port有个消息队列可以存储多个消息,而signal只能发生一次。例如,我们用自定义的源通知辅助线程进行耗时计算,signal之后,线程运行耗时函数,此时如果主线程再发送一个signal进行第二个操作,那么当上一个耗时操作结束后,辅助线程不会进行第二个操作,原因是辅助线程得到signal之后进行耗时操作,结束后将signal清除,而没有消息队列的概念,如果需要可以自己实现。而message port则有消息队列,能处理多个请求。runloop一般我们显式用到的地方就2个:NSTimer和URLRequest.
runloop是实现performOnThread的基础——目标thread必须有runloop在运行,因为该实现的原理是基于runloop源的

3. 同步的代价

posix_mutext

NSLock:lock, unlock

@synchronized

在iPhone4S上面进行100次的NSMutableArray的插入和取出测试取均值得到如下结果:

@synchronized为11ms,NSLock 8~9ms,

用GCD,也就是插入用barrier_async,get用sync,其实是和lock差不多的,大概比synchronized节省40%

(四).快速拖动图片的解决方案

快速拖动的线程方案需要和cache配合使用,一个cell需要获取图片的时候,先检查cache,如果命中则使用cache的图片,否则使用默认图片,然后在线程中加载图片,进行解码,然后在main queue中设置图片。【注意】这个方案如果不注意会有个bug,由于cell是复用的,而dispatch是并发执行的,那么可能会有cell的image顺序错乱的问题。有2个方案可以解决:1是每次cell获取新的图片时,将旧有的操作cancel掉——虽然block执行起来就无法停止,但是我们还是可以在block的执行代码中做判断控制是否要进行图片的切换操作,2是用顺序队列执行dispatch。

(五). Timer

NSTimer是基于runloop的,创建之后,只有

[NSRunLoop currentRunloop] addTimer:timer1 forMode:NSDefaultRunLoopMode],才能开始工作,对于那些没有runloop的用户线程,就无法使用这个timer了,这时可以用基于dispatch queue的

dispatch_source_set_timer。

(六). 总结

解决问题1,需要使用线程,个人没有用过,但感觉使用POSIX线程更清晰明了。

解决问题2,使用Operation Queue和GCD就够了,Operation Queue是iOS2.0加入的,而GCD是iOS4.0加的新特性,基于block在参数传递函数封装等方面比Operation更灵活,基本可以替换掉Operation Queue——不过,如果需要支持操作的cancel以及各个操作之间的依赖关系,那么只能使用Operation Queue——这个没用过。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: