文字源自对以下文章的摘抄:
- threading-programming-guide笔记一
- threading-programming-guide笔记二
- threading-programming-guide笔记三
- threading-programming-guide笔记四
感谢原作者。
这里摘抄,只为学习目的,以便日后再复习。
一、OS X和iOS中提供的不那么底层的实现多任务并发执行的解决方案:
Operation object:该技术出现在OS X 10.5中,通过将要执行的任务封装成操作对象的方式实现任务在多线程中执行。任务可以理解为你要想执行的一段代码。在这个操作对象中不光包含要执行的任务,还包含线程管理的内容,使用时通常与操作队列对象联合使用,操作队列对象会管理操作对象如何使用线程,所以我们只需要关心要执行的任务本身即可。
GCD:该技术出现在OS X 10.6中,它与Operation Object的初衷类似,就是让开发者只关注要执行的任务本身,而不需要去关注线程的管理。你只需要创建好任务,然后将任务添加到一个工作队列里即可,该工作队列会根据当前CPU性能及内核的负载情况,将任务安排到合适的线程中去执行。
Idle-time notification:该技术主要用于处理优先级相对比较低、执行时间比较短的任务,让应用程序在空闲的时候执行这类任务。Cocoa框架提供NSNotificationQueue对象处理空闲时间通知,通过使用NSPostWhenIdle选项,向队列发送空闲时间通知的请求。
Asynchronous functions:系统中有一些支持异步的函数,可以自动让你的代码并行执行。这些异步函数可能通过应用程序的守护进程或者自定义的线程执行你的代码,与主进程或主线程分离,达到并行执行任务的功能。
Timers:我们也可以在应用程序主线程中使用定时器去执行一些比较轻量级的、有一定周期性的任务。
Separate processes:虽然通过另起一个进程比线程更加重量级,但是在某些情况下要比使用线程更好一些,比如你需要的执行的任务和你的应用程序在展现数据和使用方面没有什么关系,但是可以优化你的应用程序的运行环境,或者提高应用程序获取数据的效率等。
在应用程序层面,不管是什么平台,线程的运行方式都是大体相同的,在线程的运行过程中一般都会经历三种状态,即运行中、准备运行、阻塞。
二、RunLoop
参考:threading-programming-guide笔记三
简单的来说,RunLoop用于管理和监听异步添加到线程中的事件,当有事件输入时,系统唤醒线程并将事件分派给RunLoop,当没有需要处理的事件时,RunLoop会让线程进入休眠状态。这样就能让线程常驻在进程中,而不会过多的消耗系统资源,达到有事做事,没事睡觉的效果。
Run Loop在线程中的主要作用就是帮助线程常驻在进程中,并且不会过多消耗资源。所以说Run Loop在二级线程中也不是必须需要的,要根据该线程执行的任务类型以及在整个应用中担任何作用而决定是否需要使用Run Loop。比如说,如果你创建一个二级线程只是为了执行一个不会频繁执行的一次性任务,或者需要执行很长时间的任务,那么可能就不需要使用Run Loop了。如果你需要一个线程执行周期性的定时任务,或者需要较为频繁的与主线程之间进行交互,那么就需要使用Run Loop。
使用Run Loop的情况大概有以下四点:
- 通过基于端口或自定义的数据源与其他线程进行交互。
- 在线程中执行定时事件源的任务。
- 使用Cocoa框架提供的performSelector…系列方法。
- 在线程中执行较为频繁的,具有周期性的任务。
Run Loop对象
要想操作配置Run Loop,那自然需要通过Run Loop对象来完成,它提供了一系列接口,可帮助我们便捷的添加Input sources、timers以及观察者。较高级别的Cocoa框架提供了NSRunLoop类,较底层级别的Core Foundation框架提供了指向CFRunloopRef的指针。
获取Run Loop对象
在Cocoa和Core Foundation框架中都没有提供创建Run Loop的方法,只有从当前线程获取Run Loop的方法:
- 在Cocoa框架中,NSRunLoop类提供了类方法currentRunLoop()获取NSRunLoop对象。
该方法是获取当前线程中已存在的Run Loop,如果不存在,那其实还是会创建一个Run Loop对象返回,只是Cocoa框架没有向我们暴露该接口。 - 在Core Foundation框架中提供了CFRunLoopGetCurrent()函数获取CFRunLoop对象
虽然这两个Run Loop对象并不完全等价,它们之间还是可以转换的,我们可以通过NSRunLoop对象提供的getCFRunLoop()方法获取CFRunLoop对象。因为NSRunLoop和CFRunLoop指向的都是当前线程中同一个Run Loop,所以在使用时它们可以混用,比如说要给Run Loop添加观察者时就必须得用CFRunLoop了。
配置Run Loop观察者
可以向Run Loop中添加各种事件源和观察者,这里事件源是必填项,也就是说Run Loop中至少要有一种事件源,不论是Input source还是timer,如果Run Loop中没有事件源的话,那么在启动Run Loop后就会立即退出。而观察者是可选项,如果没有监控Run Loop各运行状态的需求,可以不配置观察者。
启动Run Loop
在启动Run Loop前务必要保证已添加一种类型的事件源。在Cocoa框架和Core Foundation框架中启动Run Loop大体有三种形式,分别是无条件启动、设置时间限制启动、指定特定模式启动。
1.无条件启动
NSRunLoop对象的run()方法和Core Foundation框架中的CFRunLoopRun()函数都是无条件启动Run Loop的方式。这种方式虽然是最简单的启动方式,但也是最不推荐使用的一个方式,因为这种方式将Run Loop置于一个永久运行并且不可控的状态,它使Run Loop只能在默认模式下运行,无法给Run Loop设置特定的或自定义的模式,而且以这种模式启动的Run Loop只能通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制停止。
2.设置时间限制启动
该方式对应的方法是NSRunLoop对象的runUntilDate(_ limitDate: NSDate)方法,在启动Run Loop时设置超时时间,一旦超时那么Run Loop则自动退出。该方法的好处是可以在循环中反复启动Run Loop处理相关任务,而且可控制运行时长。
3.指定特定模式启动
该方式对应的方法是NSRunLoop对象的runMode(_ mode: String, beforeDate limitDate: NSDate)方法和Core Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)函数。前者有两个参数,第一个参数是Run Loop模式,第二个参数仍然是超时时间,该方法使Run Loop只处理指定模式中的事件源事件,当处理完事件或超时Run Loop会退出,该方法的返回值类型是Bool,如果返回true则表示Run Loop启动成功,并分派执行了任务或者达到超时时间,若返回false则表示Run Loop启动失败。后者有三个参数,前两个参数的作用一样,第三个参数的意思是Run Loop是否在执行完任务后就退出,如果设置为false,那么代表Run Loop在执行完任务后不退出,而是一直等到超时后才退出。该方法返回Run Loop的退出状态:
- CFRunLoopRunResult.Finished:表示Run Loop已分派执行完任务,并且再无任务执行的情况下退出。
- CFRunLoopRunResult.Stopped:表示Run Loop通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制退出。
- CFRunLoopRunResult.TimedOut:表示Run Loop因为超时时间到而退出。
- CFRunLoopRunResult.HandledSource:表示Run Loop已执行完任务而退出,改状态只有在returnAfterSourceHandled设置为true时才会出现。
退出Run Loop
退出Run Loop的方式总体来说有三种:
- 启动Run Loop时设置超时时间。
- 强制退出Run Loop。
- 移除Run Loop中的事件源,从而使Run Loop退出。
第一种方式是推荐使用的方式,因为可以给Run Loop设置可控的运行时间,让它执行完所有的任务以及给观察者发送通知。第二种强制退出Run Loop主要是应对无条件启动Run Loop的情况。第三种方式是最不推荐的方式,虽然在理论上说当Run Loop中没有任何数据源时会立即退出,但是在实际情况中我们创建的二级线程除了执行我们指定的任务外,有可能系统还会让其执行一些系统层面的任务,而且这些任务我们一般无法知晓,所以用这种方式退出Run Loop往往会存在延迟退出。
Run Loop对象的线程安全性
Run Loop对象的线程安全性取决于我们使用哪种API去操作。Core Foundation框架中的CFRunLoop对象是线程安全的,我们可以在任何线程中使用。Cocoa框架的NSRunLoop对象是线程不安全的,我们必须在拥有Run Loop的当前线程中操作Run Loop,如果操作了不属于当前线程的Run loop,会导致异常和各种潜在的问题发生。
自定义Run Loop事件源
Cocoa框架因为是较为高层的框架,所以没有提供操作较为底层的Run Loop事件源相关的接口和对象,所以我们只能使用Core Foundation框架中的对象和函数创建事件源并给Run Loop设置事件源。
1.创建Run Loop事件源对象
创建事件源的方法:
func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
- allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。
- order:事件源优先级,当Run Loop中有多个接收相同事件的事件源被标记为待执行时,那么就根据该优先级判断,0为最高优先级别。
- context:事件源上下文。
Run Loop事件源上下文很重要,我们来看看它的结构:
struct CFRunLoopSourceContext {
var version: CFIndex
var info: UnsafeMutablePointer<Void>
var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!
var release: ((UnsafePointer<Void>) -> Void)!
var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!
var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!
var hash: ((UnsafePointer<Void>) -> CFHashCode)!
var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!
var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!
var perform: ((UnsafeMutablePointer<Void>) -> Void)!
init()
init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!)
}
- version:事件源上下文的版本,必须设置为0。
- info:上下文中retain、release、copyDescription、equal、hash、schedule、cancel、perform这八个回调函数所有者对象的指针。
- schedule:该回调函数的作用是将该事件源与给它发送事件消息的线程进行关联,也就是说如果主线程想要给该事件源发送事件消息,那么首先主线程得能获取到该事件源。
- cancel:该回调函数的作用是使该事件源失效。
- perform:该回调函数的作用是执行其他线程或当前线程给该事件源发来的事件消息。
将事件源添加至Run Loop
事件源创建好之后,接下来就是将其添加到指定某个模式的Run Loop中,我们来看看这个方法:
func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
- rl:希望添加事件源的Run Loop对象,类型是CFRunLoop。
- source:我们创建好的事件源。
- mode:Run Loop的模式。
标记事件源及唤醒Run Loop
func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)
func CFRunLoopWakeUp(_ rl: CFRunLoop!)
这里需要注意的是唤醒Run Loop并不等价与启动Run Loop,因为启动Run Loop时需要对Run Loop进行模式、时限的设置,而唤醒Run Loop只是当已启动的Run Loop休眠时重新让其运行。
三、线程的资源消耗
线程的资源消耗主要分为三类,一类是内存空间的消耗、一类是创建线程消耗的时间、另一类是对开发人员开发成本的消耗。
内存空间的消耗又分为两部分,一部分是内核内存空间,另一部分是应用程序使用的内存空间,每个线程在创建时就会申请这两部分的内存空间。申请内核内存空间是用来存储管理和协调线程的核心数据结构的,而申请应用程序的内存空间是用来存储线程栈和一些初始化数据的。对于用户级别的二级线程来说,对应用程序内存空间的消耗是可以配置的,比如线程栈的空间大小等。
下面是两种内存空间通常的消耗情况:
- 内核内存空间:主要存储线程的核心数据结构,每个线程大约会占用1KB的空间。
- 应用程序内存空间:主要存储线程栈和初始化数据,主线程在OS X中大约占8MB空间,在iOS中大约占1MB。二级线程在两种系统中通常占大约512KB,但是上面提到二级线程在这块是可以配置的,所以可配置的最小空间为16KB,而且配置的空间大小必须是4KB的倍数。
注意:二级线程在创建时只是申请了内存程序空间,但还并没有真正分配给二级线程,只有当二级线程执行代码需要空间时才会真正分配。
四、创建线程
说到创建线程,就得说说线程的两种类型,Joinable和Detach。Joinable类型的线程可以被其他线程回收其资源和终止。举个例子,如果一个Joinable的线程与主线程结合,那么当主线程准备结束而该二级线程还没有结束的时候,主线程会被阻塞等待该二级线程,当二级线程结束后由主线程回收其占用资源并将其关闭。如果在主线程还没有结束时,该二级线程结束了,那么它不但不会关闭,而且资源也不会被系统收回,只是等待主线程处理。而Detach的线程则相反,会自行结束关闭线程并且有系统回收其资源。
五、线程属性配置
线程也是具有若干属性的,自然一些属性也是可配置的,在启动线程之前我们可以对其进行配置,比如线程占用的内存空间大小、线程持久层中的数据、设置线程类型、优先级等。
1、 配置线程的栈空间大小
- Cocoa框架:在OS X v10.5之后的版本和iOS2.0之后的版本中,我们可以通过修改NSThread类的stackSize属性,改变二级线程的线程栈大小,不过这里要注意的是该属性的单位是字节,并且设置的大小必须得是4KB的倍数。
- POSIX API:通过pthread_attr_- setstacksize函数给线程属性pthread_attr_t结构体设置线程栈大小,然后在使用pthread_create函数创建线程时将线程属性传入即可。
注意:在使用Cocoa框架的前提下修改线程栈时,不能使用NSThread的detachNewThreadSelector: toTarget:withObject:方法,因为上文中说过,该方法先创建线程,即刻便启动了线程,所以根本没有机会修改线程属性。
2、配置线程存储字典
每一个线程,在整个生命周期里都会有一个字典,以key-value的形式存储着在线程执行过程中你希望保存下来的各种类型的数据,比如一个常驻线程的运行状态,线程可以在任何时候访问该字典里的数据。
在Cocoa框架中,可以通过NSThread类的threadDictionary属性,获取到NSMutableDictionary类型对象,然后自定义key值,存入任何里先储存的对象或数据。如果使用POSIX线程,可以使用pthread_setspecific和pthread_getspecific函数设置获取线程字典。
3、配置线程类型
在上文中提到过,线程有Joinable和Detached类型,大多数非底层的线程默认都是Detached类型的,相比Joinable类型的线程来说,Detached类型的线程不用与其他线程结合,并且在执行完任务后可自动被系统回收资源,而且主线程不会因此而阻塞,这着实要方便许多。
使用NSThread创建的线程默认都是Detached类型,而且似乎也不能将其设置为Joinable类型。而使用POSIX API创建的线程则默认为Joinable类型,而且这也是唯一创建Joinable类型线程的方式。通过POSIX API可以在创建线程前通过函数pthread_attr_setdetachstate更新线程属性,将其设置为不同的类型,如果线程已经创建,那么可以使用pthread_detach函数改变其类型。Joinable类型的线程还有一个特性,那就是在终止之前可以将数据传给与之相结合的线程,从而达到线程之间的交互。即将要终止的线程可以通过pthread_exit函数传递指针或者任务执行的结果,然后与之结合的线程可以通过pthread_join函数接受数据。
虽然通过POSIX API创建的线程使用和管理起来较为复杂和麻烦,但这也说明这种方式更为灵活,更能满足不同的使用场景和需求。比如当执行一些关键的任务,不能被打断的任务,像执行I/O操作之类。
4、 设置线程优先级
不论是通过NSThread创建线程还是通过POSIX API创建线程,他们都提供了设置线程优先级的方法。我们可以通过NSThread的类方法setThreadPriority:设置优先级,因为线程的优先级由0.0~1.0表示,所以设置优先级时也一样。我们也可以通过pthread_setschedparam函数设置线程优先级。
注意:设置线程的优先级时可以在线程运行时设置。
虽然我们可以调节线程的优先级,但不到必要时还是不建议调节线程的优先级。因为一旦调高了某个线程的优先级,与低优先级线程的优先等级差距太大,就有可能导致低优先级线程永远得不到运行的机会,从而产生性能瓶颈。比如说有两个线程A和B,起初优先级相差无几,那么在执行任务的时候都会相继无序的运行,如果将线程A的优先级调高,并且当线程A不会因为执行的任务而阻塞时,线程B就可能一直不能运行,此时如果线程A中执行的任务需要与线程B中任务进行数据交互,而迟迟得不到线程B中的结果,此时线程A就会被阻塞,那么程序的性能自然就会产生瓶颈。