-
Threads的替代方案:
- Operation Objects:是一个任务包装器,这个会在非主线程执行。这个包装器隐藏了线程管理的细节,让用户可以专注在线程本身上。
- GCD,GCD可以比用thread更高效的执行任务。
- Idle-time notifications:对于优先级非常低的任务,可以考虑使用Idle-time notification。
线程在时间上和空间上创建是需要代价的,所以推荐在线程中去做非常多的重要工作或者建立run loop以允许复用一些显示任务。
Run loop是一片管理事件异步到达线程的基础设施。
-
以下是保证你代码正确性的实现线程的一些方法:
- 避免明确的建立线程。预期手动建立线程,不妨尝试使用异步API,GCD,operation objects来完成工作。
- 保持线程的忙碌:你应该确保分配给线程的人物是长周期的、多产的(不要浪费).
- 避免共享数据结构:最简单的避免线程相关资源冲突的方法是给每一个线程它需要资源的copy。平行代码在线程间最小化通信以及沟通时效率达到最大。
- 推荐在主线程去接收用户相关的事件和初始化UI。
- 留意在退出时的线程行为:进程只有在非分派的线程退出时才会停止。如果你在编写CoCoa应用,你应当使用applicationShouldTerminate:delegate方法去使得app在一段时间之后终止或者一起取消掉。
- 处理Exceptions:一些情况下,exception处理这也许会被自动创建,例如,@synchronized标签就会默认包含exception 处理器。
- 干净地终止你的线程。最佳的退出线程的方法就是让它自然退出,让它达到主链路的终点。
- 在Libraries中保证线程安全:对于Libraries 开发者,不能只在app变成多线程时创建locks。如果要在某个线程中lock,最好用libraries之前创建,推荐在创建library之前就用locks。切记Lock和unlock一定要配对。在使 * 用Cocoa library时,最好注册一个观察者接收NSwillBecomMuliThreadedNotification,这样就在application变成多线程时接收到了通知。
-
线程创建消耗:
- Kernal 数据结构:大概1KB
- 栈空间:512KB(非主线程);1MB(主线程)。最小的非主线程栈空间是16KB,并且栈空间必须是4KB的倍数。
- Creation时间:大概90毫秒。
- 另一个消耗是成品消耗。所以说,应当尽量避免使用同步,而且等待locks或者不作为更浪费时间。
如果你有正在运行的NSThread对象的化,一种可以send消息的方法是使用
performSelector:onThread:withObject:waitUntileDone:
方法。设置线程的栈空间。在Cocoa下,在调用start方法之前,使用setStackSize:方法来制定stack的size。
配置Thread-Local的存储。在Cocoa下,你可以使用NSThread对象的threadDictionary方法去接收一个NSMutableDictionary对象,理论上就可以给thread添加任何keys了。
一般情况下,将thread保留在其默认值上。Cocoa Threads,你可以使用setThreadPriority:类方法(NSThread)来设置当前运行线程的优先级。
创建Autorelease Pool:如果一个app使用GC,而不是内存管理模型,则创建autorelease pool并不是必须的。Autorelease pool在GC下并无害,但是大部分都会被忽略。Autorelease pool必须要支持managed model code,并且如果app 运行在gc下时会非常容易被忽略。所以如果你的app是运行在managed memory model下,创建一个autorelease pool应当是最先在thread实体路径中做的事儿。类似的,destory这个autorelease pool是在这个thread中最后一个要做的事。
因为top-level的autorelease pool在thread退出之前不会释放它包含的对象,所以long-lived的thread应当新建额外的autorelease pool,去更频繁的释放对象。
当你想运行在不同的线程上时,你有两个选项:第一个选项是将代码写在一个长的task上,并且几乎不被中断,结束时终止线程;另一个选项是将线程放到一个loop中,在到达时动态的执行请求,这种方法需要建立这个线程的run loop.
终止线程:推荐的终止线程的方法是让它自然的退出。虽然有killing方法,但是严重不推荐使用。如果需要中途去终止一个线程,需要设计线程去从外部响应cancel或者退出消息。对于长线的操作,需要定期检查是否有需要终止的线程。一种方式去取消msg是使用一个run loop 输入源去接受这样的msg。
while(moreWorkToDo && !exitNoew)
{
runLoop runUntilDate:[NSDate date];
exitNow =[ [threadDict valueForKey:@”ThreadShouldExitNow”] boolValue];
}
Run loop的目的是当有工作要做时,保持线程busy;当没有工作可做时让线程sleep。Runloop,字如其名,就是一个可以运行事件处理器的loop,并且这个loop可以响应未来的事件。
Runloop接收两种不同类型的源,一种是Input source,传递异步事件,接收自完全不同的application或者其它thread;另一种是Timer source,传递同步事件,在一个计划好的时间或者重复的interval中出现。
如上图所示,input source会传递异步事件给响应的处理函数并且触发runUntilDate:方法来退出。 Timer source将事件传递到handler路径上但是并不导致run loop退出。
除了处理input source之外,run loop还能生成关于run loop行为的通知。 注册 run-loop的观察者可以接收这些通知,并且可以在线程上做一些额外的操作。
Run Loop的Modes:run loop的mode是input sources和需要监听的集合,也是需要被通知的观察者的集合。每次运行run loop时,你都指定了一种特殊的mode去运行。传给run loop之后,只有符合mode条件的source才会被监视和被允许传递事件。
Mode是用来从那些不想要的source中筛选出需要的sources。大部分时间,你会希望run loop运行在default mode下。一个模态面板,也许会运行在“模态”模式下。在这种模式下,只有相关的source才会传递到那个thread上。对于非主线程而言,你也许使用custom modes来防止通过非常重要的实时的操作去传递低优先级源。
-
定义好的run loop modes:
- Default:NSDefaultRunloopMode,默认通过这个mode去配置和执行你的input sources
- Connection:NSConnectionReplyMode,这个很少用,用来俩节NSConnection对象以监听回应。
- Modal:NSModalPanelRunLoopMode,Cocoa使用这个Mode来识别为了modal panel而用的事件。
- Event tracking:NSEventTrackingRunLoopMode,Cocoa使用这个mode去限制输入事件,尤其在mouse-dragging loops或者其它类型的ui tracking loops中。
- Common modes:NSRunLoopCommonModes,这个modes是常用mode的集合。对于Cocoa 应用,这个集合包括default,modal,和event tracking mode。 CoreFoundation 默认包含default mode,你可以添加custom modes 通过set CFRunLoopAddCommonMode函数来完成。
- Input source: 输入源会将事件异步的传递到你的线程中。只要你的run loop建立了连接,input-source 是port-based还是custom就无所谓了。两种类型唯一的区别就是被通知的方式不同。Port-based是通过kernal自动的通知,而custom source必须手动通过其它线程来通知。
- Port-based Source: Cocoa和Core Fundation提供了内置的创建port-based 输入源的支持,通过使用port-related 对象和函数。在Core Foundation里,你必须手动创建ports和它的run loop source.
- Custom Input Source: 创建一个custom input source,你必须使用和CFRunLoopSourceRef相关联的function。
- Cocoa Perform Selector Source:Cocoa定义了custom input source,允许你在任何线程上去执行selector。当selector执行完毕,selector source会将自己从run loop中移除。
-
Run Loop的事件执行顺序:
- 通知已经进入run loop的observer
- 通知那些timer准备触发的observer
- 通知那些不基于port的准备触发的input source的observer
- 触发所有不基于port的input source
- 如果一个port-based 输入源准备好并等待fire,则事件会立即执行。执行第九步。
- 通知那些马上要sleep的thread的observer
- 将thread带入到sleep,除非:
i.一个基于port的input source事件来了
ii. Timer触发了
iii. Run loop被叫醒了 - 通知observers,thread已经醒了
- 执行进行中的event
- 如果用户定义的timer出发了,执行timer事件并且重启loop,跳到第二步
- 如果input source 触发了,传递事件
- 如果run loop醒了但是没有超时,重启loop,跳到第二步
通知observer,runloop退出了。
如果这些events中间的时间非常宝贵,那么你可以使用sleep和awak-from-sleep通知来帮助你关联这些真实事件之间的时间。
什么时候应当使用run loop? 只有在你为你的应用创建非主线程的时候运行一个run loop。在非主线程上,你应当决定是否应该使用run loop。如果应该,则自己配置并且start。你不应该start 一个线程的run loop,什么时候都不应该。
-
如果你想做下列事情,你应该去start一个run loop:
- 使用ports或者custom input source去和其它线程交流
- 在thread上使用timer
- 在Cocoa application上使用performSelector…
- 让线程去执行间歇性的tasks
停止一个线程,最好让它自然停止,而不是强制终止。
Start一个run loop:start一个run loop只有在你的app里的非主线程中才有必要。一个run loop必须至少有一个input source或者timer来监听。
-
有几种方法可以start一个run loop,包括:
- 无条件的
- 设置一个时间限制
- 在特殊的mode中
无条件的运行你的run loop指的是将thread放到一个永恒的loop中,你会获得非常有限的控制run loop的权利。你可以添加或者删除input source或者timer,但是stop这个run loop的唯一方式就是kill掉它。没办法在custom mode下运行run loop。
与无条件运行run loop相比,给一个时间限制更好。
-
退出run loop:有两种方法可以在运行一个事件前使得run loop退出,分别是:
- 给一个timeout值
- 告诉run loop停止。
- 其中,给一个timeout值是推荐的,前提是你可以管理它。
- 其次,虽然移除run loop的input source和timers也许也能导致run loop退出,但是这不是一个停止run loop的可靠的方法。
Cocoa NSRunLoop雷并不像Core Foundation副本一样,是一个线程安全的。
-
同步方法:
- Atomic Operations:原子操作指的是一些形式简单的同步,以简单的数据类型为媒介。原子操作的优势是他们不会block或者竞争资源。
- Memory Barriers 是一种非block型同步工具,通常用来确保memory操作以正确的顺序执行。
- Locks:可以使用Locks来保护code的重要部分,以确保在同一个时间段只有一个代码段可以被执行。
-
Lock类型有:
- Mutex:是一种确保某时间只有一个thread可以运行的信号量。
- Recursive lock:recursive lock主要用在递归操作之中,但是也会用在多个线程中每一个线程都分别需要同一个lock的情况。
- Read-write lock:这种lock可以用在大范围的操作中,而且可以显著的提高性能,尤其当保护的数据结构经常被读但是偶尔被写。(POSIX ONLY)
Conditions:Conditions最常用在确定资源是否可用或者确保tasks以一种特定order呈现。当一个thread tests 一个condition,它只会让condition为ture的运行,否则block。Condition和mutex lock之间的区别在于多线程有可能在同一时间被允许执行condition。 一种你有可能用到condition的情况是管理正在进行的事件池。
使用多线程要考虑性能损耗。也就是说,多线程下同样的代码,要比单线程下同样的代码执行效率要低,这个很难提升。
*Thread-Safe Designs:避免同时同步:对于你工作中的新项目,或者已经存在的项目,设计时让你的代码和数据结构尽量避免同步,是最好的解决方式。如果每一个操作都有自己的数据集,哪也不需要用到lock来保护数据。即便是两个tasks用到了同一个数据,那也可以看看有没有办法将数据进行分割,或者给每一个tasks对应的copy对象。小心活锁或者死锁:最好的避免活锁或者死锁的方法就是在一个时间只使用一个lock。如果你需要在同一时间用到多于一种锁,那确保其它threads不要在这个时间内做类似的事情。
正确的使用volatile变量:如果你已经用到了mutex来保护一段代码,不要想当然的认为你需要使用volatile关键字来保护重要的变量。Volatile变量强制每次从内存读取而非寄存器读取,这两个一起使用会导致性能出现问题。* 如果mutex自己就足够保护变量,则不要用volatile关键字。当然,也不要为了避免使用mutex就去使用volatile。一般来说,使用mutex或者其它同步机制比使用volatile变量要好得多。Volatile关键字只能保证变量从memory中读取,而不是通过寄存器读取
使用Atomic Operations: 虽然Locks是一种在线程中同步的有效方法,使用lock也是很消耗资源的操作,即便是在无竞争的case中。相比较,很多atomic operation使用碎片时间来完成工作,所以可以作为有效率的lock。 * Atomic operation允许你执行简单的数学操作和逻辑运算,在32-bit值或者64bit值上。例如Add、Increment、Decrement、Logical AND等等。
使用NSLock类:tryLock方法会尝试获取lock,但是如果lock不可得,不会block住;取而代之的是,方法会返回一个NO。lockBeforeDate方法会在特定时间内获取锁,如果成功返回YES,否则unlock并返回NO。
使用@synchronized关键字:@synchronized关键字做了其它mutex lock做的事:防止不同的线程在同一时间访问同一个资源。通过传给@synchronized表达式一个id,它就可以区分保护的block。
-
其它Cocoa Locks:
- NSRecursiveLock对象: RecursiveLock对象定义了一个可以被同一个线程使用多次的lock,而且不会导致死锁。顾名思义,这种锁通常用来在递归方法里防止递归不会锁住线程时使用。
- NSConditionLock对象:NSConditoinLock对象定义了一个mutex lock,只在value值为制定值时lock和unlock。 一般来说,你在使用NSConditionLock时都是当threads需要在特定的顺序执行,比如一个线程生产一个线程消费。所以消费者只在值为想要的值时锁定住生产者。
- NSDistributedLock对象:可以被用在多个app在多个hosts上,为了限制对于同一个共享资源的访问,比如file文件。 NSDistribultedLock不遵循NSLocking协议,因此没有lock方法。NSDistributedLock提供了tryLock方法让你决定是否继续进行。因为继承自文件系统,NSDistributedLock对象不会释放,除非owner自己释放。如果你的app crash了,之前恰好hold一个distributedLock,那其它客户端可能就无法访问保护的资源了。这时候,你可以使用breakLock方法去break已经存在的lock。
-
可变变量VS不可变变量:
- 不可变量一般来说是线程安全的,一旦你创建了他们,你可以从线程获取或者传给线程这些变量。
- 可变对象一般来说是线程不安全的。
- 即使一个方法声称要返回一个不可变的变量,你不能单纯的以为变量就是不可变的。如果想要确保变量是不可变的,最好进行一步immutable copy操作。
Class初始化:OC runtime系统会发送initialize消息给每一个class对象,这个过程遭遇class接收其它消息。这个方法建立了runtime 环境,在它被正式使用之前。在多线程app里,runtime 确保只有一个线程执行initialize方法,这个线程就是哪个发送给class initilaze方法的那个线程。
RunLoops:每一个线程有且自由一个run loop。如果你的app是基于Application Kit的,那么主run loop会自动运行。但是非主线程(和foundation-only app)必须自己执行run loop.
-
NSView使用限制(Mac下,和UIView是对应的):
- 你应当创建、销毁、改变大小、移动并且执行其他操作时,务必保证NSView对象在主线程之上。
- 如果非主线程上的ap想要让view的部分内容在主线程上redraw,不应该使用像display、setNeedsDisplay:,setNeedsDisplayInRect:或者是setViewsNeedDisplay等方法。相反,应该发消息给主线程或者通过performSelectorOnMainThread:withObject:waitUntilDown:方法来完成
错误的使用graphics states会导致绘画的效率低于在主线程上绘画的效率。
NSGraphicsContext使用限制:如果你在非主线程做任何drawing,一个NSGraphicsContext实例会被创建,而且是特别为那个线程量身制作。如果在非主线程做任何drawing,需要手动的清理你的drawing调用。Cocoa不会在非主线程上自动更新view的内容,所以你需要调用flushGraphics方法(NSGraphicsContext)当你完成drawing。如果你的app只在主线程draw内容,那么不需要你调用flush。