由于文章长度限制,本文作为[译]线程编程指南(一)后续部分。
Run Loops
Run loop是与线程相关的基础结构之一。Run loop是一个用来调度工作并接收事件的事件处理循环。Run loop其目的在于有任务的时候让线程保持忙碌状态而无事可做时让线程睡眠。
Run loop管理并不是完全自动的。你还必须设计出线程代码在适当的时间启动run loop,并响应传入的事件。Cocoa和Core Foundation提供run loop对象以帮助你配置和管理线程的run loop。应用程序不需要显式地创建这些对象;每一个线程,包括应用程序的主线程,都有一个相关的run loop对象。只有辅助线程才需要显式地运行它们的run loop。应用程序会自动设置和运行主线程的run loop并将其作为程序启动过程的一部分。
以下各节提供有关run loop的更多信息,以及如何在应用程序中配置它们。
剖析Run Loop
Run loop正如其名称一样,它是一个让线程进入并用来运行事件回调的循环。你的代码提供的控制语句会用于实现run loop中的实际循环部分,换句话说,你的代码中的while
和for
循环驱动着run loop的运行。在run loop内,你将使用run loop对象来驱动事件处理代码执行以接收事件和调用处理回调。
Run loop通过两种源来接收事件。输入源传递异步的事件,通常是其他线程和另一个应用传入。定时源传递同步事件,发生在调度时期或者重复周期。**当相关事件到达时两种类型的源都会使用应用指定的回调例程来处理事件。
图3-1展示了run loop及其不同类型源的概念结构。输入源传递异步事件给相应的回调并引起runUntilDate:
方法调用(实际由线程的NSRunLoop对象调用)以退出run loop。定时源传递事件给回调例程但并不导致run loop退出。
图3-1 Run loop及其源结构
[图片上传失败...(image-725270-1536734662071)]
除了处理输入源之外,run loop同样会为其行为创建通知。注册一个run loop的观察者可以获取到这些通知并使用它们在线程中做进一步的操作。
以下章节将提供有关run loop构成和该模式如何操作的更多细节。同样会描述事件处理的不同阶段通知的创建过程。
Run Loop模式
一个run loop的模式包括了一系列待监测的输入源、定时源以及待通知的观察者。每次运行run loop时,都会(显式或隐式地)为其指定一个运行“模式”。在run loop过程中,只监测与该模式相关联的源,并允许相关的事件传递。(类似地,只有与该模式相关联的观察者通知run loop执行)与其他模式相关联来源于任何新事件的消息,直到随后在适当模式下才进入循环。
在代码当中,使用名称来定义模式。Cocoa和Core Foundation定义了默认模式和几个常用模式,在代码中都是以字符串的形式表示。你也可以定义一个自定义模式并用字符串为模式命名。虽然自定义模式的名称是任意的,但模式的内容却不是。你必须确认为其加入一个或多个输入源、定时源、观察者才会使其产生真正的效果。
你可以使用模式来过滤掉run loop中不感兴趣的事件。多数情况下,你会希望run loop运行在系统默认定义的“default”模式下。对于“模态面板”,可能会运行在“模态”模式下。当进入该模式时,只有源相关的模态面板才能在线程中传递事件。对于辅助线程而言,你需要使用自定义模式来防止低优先级源在时间敏感的操作中传递事件。
注意:模式基于事件的源区分而不是事件类型。例如,你不能使用模式来仅匹配鼠标点击事件或者键盘事件。你应该使用模式来监听不同的端口,偶尔挂起定时器,或者切换当前被监测源和观察者。
表3-1列举了Cocoa和Core Foundation中的标准模式及其如何使用的描述。名称字段列举了在代码中使用该模式的名称常量。
表3-1 预定义的run loop模式
模式 | 名称 | 描述 |
---|---|---|
默认(Default) | NSDefaultRunLoopMode (Cocoa)、kCFRunLoopDefaultMode (Core Foundation) | 大多数操作使用的默认模式。大多数时间,你会使用该模式启动run loop并配置输入源。 |
连接(Connection) | NSConnectionReplyMode (Cocoa) | Cocoa使用该模式来连接NSConnection对象以监视其回应。你自身很少会用到这种模式。 |
模态(Modal) | NSModalPanelRunLoopMode (Cocoa) | Cocoa使用该模式来追踪相关模态面板的事件。 |
事件追踪(Event tracking) | NSEventTrackingRunLoopMode (Cocoa) | Cocoa使用该模式来限制其他事件进入当鼠标拖动期间以及其他用户界面跟踪期间。 |
通用模式(Common modes) | NSRunLoopCommonModes (Cocoa)、kCFRunLoopCommonModes (Core Foundation) | 这是一个可配置的通用模式组。为其中的一个模式关联输入源的同时会为该组中的所有模式关联该输入源。对于Cocoa应用,这个集合默认包含了上述全部模式。对于Core Foundation来讲则最初只包含了默认模式。你可以使用CFRunLoopAddCommonMode 函数向其添加自定义模式。 |
输入源
输入源在你的线程中异步地传递事件。事件的来源取决于输入源的类型,通常是两种中的一个类型。基于端口的输入源在你应用的Mach端口上进行监听。自定义的输入源监听自定义的事件源。只要是涉及到run loop,一个输入源是基于端口的或是自定义的并不重要。系统通常会实现两种类型以供使用。这两种源唯一区别在于它们通知方式不同。基于端口的源由内核自动通知,自定义的源需要从其他线程手动通知。
当你创建完输入源之后,可以将其指定到run loop的一个或多个模式下。在任何时刻模式都会影响到输入源的受监视情况。在大多数时间,你会在默认模式下运行run loop,但是你仍可以指定到自定义模式下运行。如果输入源不在当前监视的模式下,任何它产生的事件会被保留直到下一次run loop运行到正确的模式。
以下具体描述了各种类型的输入源。
基于端口的输入源
Cocoa和Core Foundation提供了使用基于端口的对象和函数来创建基于端口输入源的内建支持。例如,在Cocoa中,你不必直接创建输入源。你仅需使用NSPort的方法创建端口对象并将该端口添加到run loop。端口对象会为你处理创建和配置所需的输入源。
在Core Foundation中,你必须同时手动创建端口和run loop源。在这种情况下,你可以使用端口相关的不透明类型(CFMachPortRef,CFMessagePortRef,或CFSocketRef)的函数来创建合适的对象。
自定义输入源
为创建自定义输入源,你必须使用Core Foundation中CFRunLoopSourceRef类型相关的函数,并使用多个不同的回调函数来配置自定义的输入源。Core Foundation调用这些函数来配置源,处理任何到达事件,以及当源从run loop移除时销毁它们。
当事件到达时,定义自定义源的行为的同时,你必须定义事件的传递机制。源的这部分运行在一个单独的线程中,负责为输入源提供数据,并在数据准备好时发出信号。事件传递机制是取决于需求而不必过于复杂。
Cocoa执行选择器源
除了基于端口的输入源之外,Cocoa定义了一种可以允许在任何线程上执行选择器的自定义输入源。同基于端口的输入源类似,执行选择器的请求被序列化到目标线程,缓解了同一线程上运行的多个方法间可能存在的同步问题的产生。与基于端口的源不同的是,执行选择器的源会在执行完选择器之后将自身从run loop上移除。
注意:早在OS X 10.5之前,执行选择器源被主要用于主线程的消息发送上,但在OS X 10.5之后以及iOS上,你可以使用它们向任何线程发送消息。
当在另一个线程上执行选择器时,目标线程必须有一个活动的run loop。这意味着线程一旦创建,你的代码就必须显式地开启一个run loop。由于主线程会开启它自身的run loop,然而,你也可以在应用的委托回调方法applicationDidFinishLaunching:
完成该调用。Run loop会在每次通过循环时完成所有队列中的选择器调用,而不是在每次循环中只处理一个。
表3-2列举了NSObject中可用于在其他线程上执行选择器的方法。由于这些方法都由NSObject声明,所以你能够在任何能访问到Objective-C对象的线程上使用它们,包括POSIX线程。这些方法事实上不会创建新的线程来执行选择器。
表3-2 在其他线程上执行选择器
名称 | 描述 |
---|---|
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes:
|
在应用主线程的下一个run loop周期内执行指定的选择器。该方法会阻塞当前线程直到选择器执行完毕。 |
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes:
|
在任何一个NSThread对象的线程上执行指定选择器。该方法会阻塞当前线程直到选择器执行完毕。 |
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes:
|
在当前线程的下一个run loop周期内的选择性延迟时间后执行指定的选择器。由于它会等到下一个run loop的周期才执行,所以该方法提供了一个从当前起自动的最小延迟执行时间。队列中的多个选择器在排列好后会依次执行。 |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object:
|
取消上一个执行请求。 |
定时源
定时源在将来的一个预定时间向线程同步地传递事件。定时器作为线程通知自身完成某些工作的一种方式。例如,搜索框一旦在用户连续输入关键字间隙可以使用计时器来发起自动搜索。在开始搜索之前,搜索操作的延时可以让用户尽可能多的输入所需的关键字符串。
尽管定时源创建了基于时间的通知,但定时器并不是一个实时机制。类似输入源,定时器与run loop的具体模式关联。如果定时器在run loop模式中当前没有处于被监控状态,它会直到run loop运行到支持的模式下才开启。同理,如果定时器在run loop执行到回调中是触发,该定时器会等到下一次进入run loop时才回去调用回调。如果run loop根本就没有运行,定时器永远不会被触发。
你可以创建定时器以一次或者重复多次地产生事件。重复性的定时器会自动基于其调度触发时间重新调度,而并不是实际的触发时间。例如,如果一个定时器原定于每5秒完成一次触发,调度触发时间总是落在原有的5秒间隔上,即便实际的触发时间产生了延迟。如果触发时间被延迟过多则会导致其错过一次或多次的调度触发机会,使得定时器在某一段时间内只触发了一次。在调度丢失的时间完成触发之后,定时器会在下一次调度触发时间重新调度。
Run Loop观察者
对比那些在合适时间触发或同步或异步的事件的源,run loop观察者则在run loop自身执行的特定位置触发事件。你可以使用run loop观察者对线程的既定事件做准备或者在线程睡眠之前做好准备。你可以在run loop中为观察者关联如下事件:
- Run loop进入。
- Run loop将要处理定时器。
- Run loop将要处理输入源。
- Run loop将要睡眠。
- Run loop将要唤醒,但在其处理事件之前。
- Run loop退出。
你可以使用Core Foundation提供的方式来为应用创建run loop观察者。为创建run loop观察者,你需要创建CFRunLoopObserverRef
类型的实例。该类型会追踪自定义的回调函数以及感兴趣的活动。
和定时器一样,观察者可以被一次性或重复性的使用。一次性观察者在触发之后会将自己从run loop中移除,而重复执行的观察者则继续保留。你可以在观察者创建时指定其是一次性的还是重复性的。
Run Loop事件队列
在每次run loop运行时,线程的run loop执行挂起事件并为任何关联的观察者创建通知。这些事件的通知顺序如下:
- 通知观察者run loop进入。
- 通知观察者就绪的定时器即将触发。
- 通知观察者non-port-based(非基于端口)输入源即将触发。
- 触发已就绪的non-port-based输入源。
- 如果基于端口的输入源等到触发,立即处理该事件。跳到步骤9。
- 通知观察者线程即将睡眠。
- 保持线程睡眠直到下列其中一个事件发生:
- 基于端口的输入源事件到达。
- 定时器触发。
- Run loop超时。
- Run loop被显式地唤醒。
- 通知观察者线程唤醒。
- 处理挂起事件。
- 如果用户定义的定时器触发,处理定时器事件并重启run loop。跳到步骤2。
- 如果输入源触发,传递该事件。
- 如果run loop显式地唤醒且并不超时,重启run loop。跳到步骤2。
- 通知观察者run loop已退出。
由于观察者对定时器和输入源的通知是在相应事件实际发生之前传递的,所以通知的时间和事件实际发生的时间可能存在间隔。如果这些事件之间的时间至关重要,你可以使用sleep和awake-from-sleep通知来帮助你关联这些事件之间的时间。
由于定时器和其他周期性的事件在run loop运行时被传递,从而避免了循环在传递事件时被中断。这种情况典型的例子就是你在进入run loop之后实现了一个鼠标追踪回调,然后你会重复地从应用中收到该事件。由于你的代码是直接抓取事件,而不是让应用来常规地分发那些事件,活动的定时器并不会触发直到退出鼠标追踪回调之后并返回应用控制时。
使用run loop对象可以显式地唤醒run loop。其他事件也有可能造成run loop的唤醒。如,加入其他的non-port-based输入源会唤醒run loop以便使得输入源(事件)能够被立即处理,而不是等到事件发生时才这样做。
何时使用Run Loop?
只有当你为应用创建辅助线程时才会需要显式地运行一个run loop。应用程序主线程的run loop是一个十分关键的结构。因此,应用框架会提供主线程run loop自动开始的代码支持。iOS中UIApplication(或者OS X中NSApplication)的run
方法会启动主线程run loop作为常规启动队列中的一步。如果你使用Xcode的项目模板来创建应用,你不必显式地调用这些例程。
对于辅助线程而言,需要先决定run loop是否必须,如果是这样,才去配置并启动它。你不必在所有情况下为线程开启run loop。例如,如果你使用线程做某个长期且预定的工作,也许可以避免开启run loop。Run loop在更多情况下用在与线程做额外交互的��场景中。当你计划做以下工作时你需要开启run loop:
- 使用基于端口或自定义输入源来和其他线程交流时。
- 线程上使用定时器时。
- 在Cocoa应用中使用任何
performSelector...
相关的方法时。 - 保持线程执行周期性的任务时。
如果你选择使用run loop,其创建和配置是显而易见的。正如所有线程编程技术一样,你应该有一个在合适场景下退出辅助线程的计划,干净地退出线程总是比强制终止它更好。
使用Run Loop对象
Run loop对象提供了向其添加输入源、定时器、观察者并运行它的主接口。每个线程有单独的一个run loop对象与其关联。在Cocoa中,该对象作为NSRunLoop的实例。在低等级应用中,它是一个指向CFRunLoopRef类型的指针。
获取Run Loop对象
为获得当前线程的run loop,可以采取下列方式中的其中一种:
- 在Cocoa应用中,使用NSRunLoop的
currentRunLoop
类方法获取NSRunLoop对象。 - 使用
CFRunLoopGetCurrent
函数。
尽管它们不是免费桥接的类型,当需要时还是可以从NSRunLoop对象中获取CFRunLoopRef类型。NSRunLoop定义了getCFRunLoop
方法以返回可传递给Core Foundation例程的CFRunLoopRef类型。由于两种对象都是指向相同的run loop,你可以根据需求混合调用NSRunLoop对象和CFRunLoopRef类型。
配置Run Loop
在辅助线程上运行run loop之前,你必须为其添加至少一个输入源或定时器。如果run loop没有任何源来监测,它会在你试图启动它时立即退出。
除了安装源之外,你同样可以安装run loop观察者并使用它们来检测run loop执行的不同阶段。为创建run loop观察者,你需要创建一个CFRunLoopObserverRef类型并使用CFRunLoopAddObserver
函数将其添加到run loop中。Run loop观察者必须使用Core Foundation创建,即便是Cocoa应用中。
代码3-1展示了run loop中绑定观察者的主例程代码。该示例的目的在于展示如何创建run loop观察者,所以代码简单地设置了一个监测所有run loop事件的观察者对象。基本的回调例程(没有显示)被简化用日志输出的方式代替。
代码3-1 创建run loop观察者
- (void)threadMain
{
// 该应用使用垃圾回收处理,所以不需要自动释放池
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建观察者并绑定到run loop
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// 创建并调度定时器
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(doFireTimer:)
userInfo:nil
repeats:YES];
NSInteger loopCount = 10;
do
{
// 运行10次run loop后触发定时器
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
当为长周期线程配置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的唯一方式是终止它。同样在自定义模式下不能运行run loop。
除开无条件地运行run loop,最好设置一个超时时间后运行run loop。当设置时间阈值后,run loop会在事件到达或时间超时后运行。当事件到达,事件会被分发给处理回调然后run loop退出。你的代码可以在之后重启run loop以处理下一次事件。如果是分配时间过期,可以简单地重新启动run loop或重设时间来做任何需要的事务管理。
除了设置时间阈外,你同样可以用指定的模式来运行run loop。模式和时间阈并不互斥并可以同时用于run loop启动。模式会对传递事件的来源做出限制。
代码3-2展示了线程主入口例程的框架版本。该示例的关键部分展示了run loop的基本结构。在本质上,你将输入源和计时器添加到run loop,然后重复调用例程之一以启动run loop。每次run loop例程返回时,请检查是否出现了可能需要退出线程的任何条件。示例使用Core Foundation的run loop例程,以便它可以检查返回结果,并确定run loop退出的原因。如果使用的是Cocoa方式且不需要检查返回值,你也可以使用NSRunLoop类的方法以类似的方式运行run loop。(使用NSRunLoop类的方法完成run loop调用,示例参见代码3-14。)
代码3-2 运行run loop
- (void)skeletonThreadMain
{
// 如果不使用垃圾回收处理需设置一个自动释放池
BOOL done = NO;
// 为run loop添加输入源或定时器并做其他设置
do
{
// 启动run loop并在每个源完成处理后返回
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// 如果run loop被源显式地停止,或者不存在输入源或定时器,则退出run loop
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
{
done = YES;
}
// 检查其他的退出条件并修改done变量
}
while (!done);
// 这里编写清理代码,确保释放自动释放池中的对象
}
递归地运行run loop是可能的情况。换句话说,你可以调用CFRunLoopRun
,CFRunLoopRunInMode
,或任何NSRunLoop方法从输入源或定时器的回调例程内开始run loop。这样做时,你可以使用任何想要运行嵌套run loop的模式,包括使用外部run loop的模式。
退出Run Loop
有两种退出run loop的方式:
- 配置run loop运行超时时间阈值。
- 通知run loop退出。
使用超时的方式当然是首选,如果你能控制它。指定超时阈可以让run loop结束所有的正常处理,包括在退出之前向run loop的观察者传递通知。
使用CFRunLoopStop
函数显式地停止run loop产生的效果类似于超时。Run loop发出任何剩余的通知,然后退出。其区别在于,你可以在run loop无条件启动的情况下使用这种技术。
虽然移除run loop的输入源和定时器也可能导致其退出,但这不是退出run loop的可靠方式。一些系统例程将输入源添加到run loop来处理所需事件。因为你的代码可能不知道这些输入源,所以无法删除它们,这会阻止run loop退出。
线程安全与Run Loop对象
线程安全取决于使用哪个接口来操作你的run loop。Core Foundation上的功能通常是线程安全的,并且可以从任何线程调用。如果你正在执行更改run loop配置的操作,那么,它仍然是很好的做法,这样就可以在任何可能的时候都拥有run loop的线程了。
Cocoa的NSRunLoop类并不如它在Core Foundation中的副本(译者注:CFRunLoopRef类型)那样线程安全。如果你正在使用NSRunLoop类修改你的run loop,你应该只在同一个线程拥有的run loop这样做。向属于不同线程的run loop添加输入源或定时器,可能导致你的代码崩溃或表现异常。
配置Run Loop源
以下各节将展示Cocoa和Core Foundation中如何设置输入源的类型。
定义自定义输入源
创建自定义输入源涉及到以下定义:
- 需要输入源处理的信息。
- 让感兴趣的客户端与输入源交互的调度器例程。
- 处理客户端请求回调的例程。
- 注销输入源的取消例程。
因为你创建了自定义的输入源来处理自定义信息,所以实际的配置是灵活的。调度程序,处理程序和取消例程几乎是所需自定义输入源的全部关键例程。然而大多数输入源的行为发生在这些处理程序以外的例程。例如,你定义了一个用于输入源向其他线程传递数据和信息交换的机制。
图3-2展示了一个自定义的输入源配置示例。在这个例子中,应用程序的主线程维护着输入源的引用,输入源的自定义命令缓冲区,以及安装了输入源的run loop。当主线程有一个需交由工作线程的任务时,它将向命令缓冲区发出命令以及工作线程所需要的任何信息来启动任务。(由于主线程和工作线程的输入源都可以访问命令缓冲区,所以访问必须是同步的。)一旦命令被发出,主线程会向输入源发出信号,并唤醒工作线程的run loop。在接收唤醒命令时,run loop调用该输入源的处理程序,输入源会处理在命令缓冲区中找到的命令。
图3-2 自定义输入源的操作流程
[图片上传失败...(image-675d05-1536734662071)]
以下部分阐释了自定义输入源的实现,并显示了需要实现的关键代码。
定义输入源
定义自定义的输入源需要使用Core Foundation的例程,以此来对run loop源进行配置和绑定。尽管基本的回调都是基于C语言的函数,这并不意味着你需要为那些函数编写Objective-C或C++的代码包装。
图3-2中所示的输入源使用Objective-C对象管理命令缓冲区,并和run loop进行调节工作。代码3-3展示了输入源对象的定义。RunLoopSource
对象管理着命令缓冲区并使用该缓冲区来接收其他线程的消息。该段代码同样展示了RunLoopContext
对象的定义,该对象作为传递RunLoopSource
对象的容器对象以及run loop对应用主线程的引用对象。
代码3-3 自定义输入源对象定义
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
// Handler method
- (void)sourceFired;
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
虽然Objective-C代码管理着输入源的自定义数据,将输入源绑定到run loop需要基于C的回调函数。这些函数在run loop源绑定到run loop后有一次初次调用,并在代码3-4展示。因为这个输入源只有一个客户端(主线程),它会调用调度函数来向线程发送一个注册自身应用委托对象的消息。当委托对象希望与输入源通信时,它会利用在RunLoopContext
对象中的信息来完成。
代码3-4 调度run loop源
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj
andLoop:rl];
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext
waitUntilDone:NO];
}
最重要的回调处理例程是输入源被通知时处理自定义数据的例程。代码3-5展示了该例程与RunLoopSource
的关联。该函数简单地发起了一个名为sourceFired
的方法的工作请求,sourceFired
方法内部会处理命令缓冲池中存在的任何命令。
代码3-5 输入源中执行任务
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果你一旦调用CFRunLoopSourceInvalidate
函数将输入源从run loop中移除,系统会调用输入源的取消例程。你可以在该例程中通知客户端输入源将要失效并让其移除对输入源的引用。代码3-6展示了使用RunLoopSource
对象注册的取消回调例程。该函数向应用的委托对象发送了另一个RunLoopContext
对象,但这次是让请求对象移除对run loop源的引用。
代码3-6 停止输入源
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext
waitUntilDone:YES];
}
注意:应用委托对象的
registerSource:
和removeSource:
方法代码在之前的代码3-8中展示。
为Run Loop安装输入源
代码3-7展示了RunLoopSource类的init
和addToCurrentRunLoop
方法。init
方法创建了与run loop实际绑定的CFRunLoopSourceRef类型。它将RunLoopSource对象作为上下文信息传递以让回调例程持有该对象的指针。输入源的安装直到工作线程调用addToCurrentRunLoop
方法才发生,这时RunLoopSourceScheduleRoutine
回调会被调用。一旦输入源被添加到run loop,线程会运行run loop并在run loop上做好准备。
代码3-7 安装输入源
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
调节客户端与输入源
为了使输入源有用,你需要对其操作并在其他线程上向其发出信号。当无事可做时输入源会让关联的线程处于睡眠状态。这需要在应用中的其他线程上知道该输入源并如何和它通信。
其中的一种方式就是在输入源初次安装到run loop时通知客户端输入源将要发出注册的请求。你可以为你的输入源注册尽可能多的客户端,或者简单地向中心对象注册然后向感兴趣的客户端“售出”输入源。代码3-8展示了应用委托对象内定义的注册方法,并在RunLoopSource对象的调度函数调用。该方法从RunLoopSource对象获取RunLoopContext对象并将其添加到它的多个源当中。代码也展示了将输入源从run loop注销的函数例程。
代码3-8 使用应用委托对象完成输入源注册与移除
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
{
[sourcesToPing removeObject:objToRemove];
}
}
注意:调用这些方法的回调函数代码在之前的代码3-4和代码3-6中展示。
向输入源发出信号
在向输入源传递数据之后,客户端必须向源发起信号并唤醒其run loop。向源发起信号的目的是让run loop知道源已经准备好处理了。还有一个原因就是线程可能在发起信号时处理睡眠状态,你应该总是显式地唤醒run loop。如果没能完成这样的操作,可能会导致输入源延迟处理。
代码3-9展示了RunLoop的fireCommandsOnRunLoop
方法。客户端在输入源做好准备处理缓冲区命令时调用该方法。
代码3-9 唤醒run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
注意:你不能通过自定义输入源尝试处理
SIGHUP
或其他类型的进程级信号。Core Foundation中唤醒run loop的函数并不安全且不应该在应用的信号处理例程中使用。
配置定时源
为创建定时器源,所有你需要做的就是创建一个定时器对象并完成它在run loop上的调用。在Cocoa中,可以使用NSTimer类创建新的定时器对象,在Core Foundation中则使用CFRunLoopTimerRef类型。本质上,NSTimer类是Core Foundation上提供的一种更加便利功能的扩展,如在同一个方法上创建并调度定时器的能力。
在Cocoa中,你可以用以下的类方法同时完成定时器的创建和调度:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
这些方法会创建定时器并将其添加到默认模式下的run loop中。(NSDefaultRunLoopMode)你也可以按照需求手动地调度定时器,这需要使用NSRunLoop的addTimer:forMode:
方法来创建NSTimer对象并将其添加到run loop。这两种技术都能够完成基本的操作但给你对于定时器不同层级的控制。例如,如果创建定时器并手动地添加到run loop,则可以使用除默认模式外的其他模式。代码3-10展示了如何使用这两种技术来创建定时器。第一个定时器有1秒的创建延迟并在之后每隔0.1秒触发一次。第二个定时器在初始化后的0.2秒触发并在之后每隔0.2秒触发一次。
代码3-10 使用NSTimer创建并调度定时器
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];
代码3-11展示了使用Core Foundation函数对定时器完成配置的代码。尽管该示例在上下文并不传递任何用户定义的信息,你可以使用该上下文为定时器传递任何自定义的数据。
代码3-11 使用Core Foundation创建并调度定时器
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
配置基于端口的输入源
Cocoa和Core Foundation同时提供了用于线程或进程间通信的基于端口的对象。下面部分将向你展示如何使用不同类型的端口来实现端口通信。
配置NSMachPort对象
为了使用NSMachPort对象建立本地连接,你需要创建一个端口对象并添加到主要线程的run loop中。当启动辅助线程后,向该线程的入口点函数传递相同的端口对象。辅助线程就可以使用这个对象来向主要线程发送消息。
主线程代码实现
代码3-12展示了如何启动辅助的工作线程的主线程代码。由于Cocoa框架实现了许多配置run loop端口的中介步骤,launchThread
方法明显比Core Foundation中等价的函数(代码3-17)短;然而它们两者行为近乎等同。唯一的区别在于该方法直接发送NSPort对象,而不是向工作线程发送本地端口的名字。
代码3-12 主线程的启动方法
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];
// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort
forMode:NSDefaultRunLoopMode];
// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class]
withObject:myPort];
}
}
为了实现线程间双边通信的信道,你应该想要工作线程在向主线程发送的检入信息中包含它自己的本地端口。接收到该检入信息能够让你的主线程知道辅助线程的启动状况以及让你向其发送额外的信息。
代码3-13展示了主线程中的handlePortMessage:
方法。该方法在数据到达本地端口时调用。当检入信息到达时,该方法为辅助线程从端口信息中获取端口并将其存储起来以供使用。
代码3-13 处理Mach端口消息
#define kCheckinMessage 100
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;
if (message == kCheckinMessage)
{
// Get the worker thread’s communications port.
distantPort = [portMessage sendPort];
// Retain and save the worker port for later use.
[self storeDistantPort:distantPort];
}
else
{
// Handle other messages.
}
}
辅助线程代码实现
对于辅助工作线程,你必须为配置该线程并使用指定端口来和主线程完成信息的回复。
代码3-14展示了该工作线程的设置代码。在为线程创建自动释放池后,该方法创建了一个工作者对象并驱动线程的执行。工作对象的sendCheckinMessage:
方法(代码3-15展示)创建了一个工作线程的本地端口并发送检入消息给主线程。
代码3-14 启动使用Mach端口的辅助线程
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;
MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];
// Let the run loop process things.
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
}
当使用NSMachPort时,本地及远程的对象可以使用同样的端口对象进行线程间的通信。换句话讲,由线程创建的本地端口对象可以成为其他线程的远程端口。
代码3-15展示了辅助线程中的检入例程。该方法设置了一个用于单边通信的本地端口,并在之后向主线程回送了检入信息。该方法使用从LaunchThreadWithPort:
方法接收到的端口对象作为消息的目标对象。
代码3-15 使用Mach端口发送检入信息
// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
// Retain and save the remote port for future use.
[self setRemotePort:outPort];
// Create and configure the worker thread port.
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// Create the check-in message.
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort
components:nil];
if (messageObj)
{
// Finish configuring the message and send it immediately.
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
}
配置NSMessagePort对象
为创建基于NSMessagePort对象的本地连接,你不能在线程间简单地传递端口对象。远程消息端口根据名称获得。Cocoa中需要本地端口注册指定的名称并将名称传递给远端的线程以便其获得用于通信的合适的端口对象。代码3-16展示了消息端口的创建与注册流程。
代码3-16 注册一个消息端口
NSPort* localPort = [[NSMessagePort alloc] init];
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
Core Foundation配置基于端口输入源
本部分将展示如何使用Core Foundation在主线程和工作线程之间设置一个双边通信的信道。
代码3-17展示了主线程启动工作线程的调用代码。该段代码首先创建了一个CFMessagePortRef类型来监听工作线程上的消息。由于工作线程需要端口的名称来建立连接,所以工作线程的入口点函数需要传入字符串值。端口名称在当前用户的环境下必须是唯一的;否则可能会遇到冲突。
代码3-17 为新线程绑定Core Foundation消息端口
#define kThreadStackSize (8 *4096)
OSStatus MySpawnThread()
{
// Create a local port for receiving responses.
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
// Create a string with the port name.
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
// Create the port.
myPort = CFMessagePortCreateLocal(NULL, myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
// The port was successfully created.
// Now create a run loop source for it.
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (rlSource)
{
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
}
}
// Create the thread and continue processing.
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint, (void*)myPortName,
kThreadStackSize, NULL, NULL, NULL, 0, &taskID));
}
在端口安装完成和线程启动之后,主线程可以继续它的常规执行以等待线程的检入。当检入消息到达时,它会被分发到主线程的MainThreadResponseHandler
函数,在代码3-18中显示。该函数从工作线程中提取端口的名称并创建好将来通信的渠道。
代码3-18 接收检入消息
#define kCheckinMessage 100
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid,
CFDataRef data,
void* info)
{
if (msgid == kCheckinMessage)
{
CFMessagePortRef messagePort;
CFStringRef threadPortName;
CFIndex bufferLength = CFDataGetLength(data);
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
// You must obtain a remote message port by name.
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
if (messagePort)
{
// Retain and save the thread’s comm port for future reference.
AddPortToListOfActiveThreads(messagePort);
// Since the port is retained by the previous function, release
// it here.
CFRelease(messagePort);
}
// Clean up.
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// Process other messages.
}
return NULL;
}
完成主线程的配置后,唯一剩下的事情就是对新创建工作线程创建它自己的端口并检入。代码3-19展示了工作线程的入口函数。该函数从主线程中提取到端口名并使用它来创建回连主线程的端口。该函数然后创建了自己的安装在run loop上的一个本地端口,并向主线程发送一条包含本地端口名的检入消息。
代码3-19 创建线程结构
OSStatus ServerThreadEntryPoint(void* param)
{
// Create the remote port to the main thread.
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
// Free the string that was passed in param.
CFRelease(portName);
// Create a port for the worker thread.
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
// Store the port in this thread’s context info for later reference.
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);
if (shouldFreeInfo)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
// Package up the port name and send the check-in message.
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
CFStringGetBytes(myPortName, CFRangeMake(0,stringLength),
kCFStringEncodingASCII, 0, FALSE, buffer, stringLength, NULL);
outData = CFDataCreate(NULL, buffer, stringLength);
CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
// Clean up thread data structures.
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
// Enter the run loop.
CFRunLoopRun();
}
一旦它进入run loop,所有将来发向该线程端口的事件都将由ProcessClientRequest
函数处理。该函数的实现依赖于线程的具体工作且不在这里做展示。
线程同步
在应用程序中多线程的存在下,多线程的执行产生了安全访问资源的潜在问题。两个线程可能不经意间就会修改相同的资源。例如,一个线程可能覆盖另一个线程的更改,或者将应用程序置于一个未知的和潜在的无效状态。如果你比较幸运,损坏的资源可能会导致明显的性能问题或崩溃,这比较容易跟踪和修复。但是如果你比较不幸,这导致的微小错误并不能即时被察觉或错误需要代码修改多处才能解决。
说到线程安全,一个好的设计才是最好的保护方式。避免资源共享和减少线程之间的交互,使这些线程间不太那么容易相互干扰。然而一个完全避免干扰的设计是不可能的。在线程必须进行交互的前提下,你需要使用同步工具来保证它们之间的交互是安全的。
OS X和iOS提供众多的同步工具供你使用,工具从提供互斥访问到事件队列。下面的章节将介绍这些工具以及如何在代码中使用它们,以解决程序资源安全访问的问题。
同步工具
为了防止线程超出预期的修改数据,可以对应用的设计做出修改或者使用同步工具避免同步问题。虽然完全避免同步问题是可取的,但它并不总是可能的。下面的章节描述了可供你使用的同步工具的基本类别。
原子操作
原子操作是作用于简单数据类型上的简单同步形式。其优势在于它不阻塞相互竞争的线程。对于简单的操作,譬如增加计数器变量,它比锁的性能更加优越。
OS X和iOS包括了多个用于32位及64位基本数学运算和逻辑运算的操作。在这些操作中属于原子性的有compare-and-swap、test-and-set,以及test-and-clear。请查看/usr/include/libkern/OSAtomic.h头文件或查看atomic的man帮助页获取原子操作支持的列表。
内存屏障与Volatile变量
为达到最优性能,编译器时常记录汇编级别的指令以使处理器能尽可能满载地处理流水线指令。作为该优化的一部分,编译器会记录那些不会造成错误数据的主内存访问指令。不幸的是,编译器不能总是检测到所有的内存独立操作。如果看似独立的变量在实际上却相互影响,编译器就会以错误的顺序对其更新,产生潜在的错误结果。
内存屏障作为一种非阻塞式的同步工具,其用于确保内存操作处于正确顺序。内存屏障的角色近乎于“栅栏”,它会强制处理器完成任何位于它之前载入和存储的操作,而将其后的操作阻挡在外。内存屏障典型地应用于确保线程上内存操作(操作结果对其他线程可见)总是按照预定顺序执行。如果在刚才的情形下缺少了内存屏障,这将导致其他线程得到不可预计的结果。(译者注:这里说的情形是读写者问题)为使用内存屏障,你可以在代码合适的位置简单地调用OSMemoryBarrier
函数。
Volatile变量应用于对于单独变量的内存限制。编译器优化代码时常通过将变量的值载入寄存器中。对于本地变量,这通常没有什么问题。如果一个变量对于其他线程可见,那么这样的优化也许会对其他线程对于该变量值修改的观察造成影响。对变量使用volatile
关键字可以强制编译器每次使用变量时从内存中载入。你可以在变量可能任何时候被外部修改且编译器不能检测到时考虑声明变量为volatile。
锁
锁作为最为常用的同步工具。你可以使用锁来保护代码的临界区,临界区仅能在同一时刻被单个线程访问。例如,临界区在同一时刻可能会操作一个特殊的数据结构或者占用只支持一个用户使用的资源。在该区域加入锁,你就可以排除其他线程造成的修改和对代码正确性造成影响。
表4-1列举了程序猿常用的锁方式。OS X和iOS提供了其中大多数锁的实现,但并不是全部。对于不支持的锁类型,描述字段会解释其未在该平台直接实现的原因。
表4-1 锁类型
锁类型 | 描述 |
---|---|
互斥锁(Mutex) | 互斥排他(或互斥)锁作为资源的保护屏障。互斥锁是一种信号量类型并在同一时刻只为一个线程授权访问资源。如果使用了互斥锁然而其他线程试图请求它,那个线程会阻塞直到互斥锁被它原先的持有者释放。如果多个线程竞争同一个互斥锁,只有其中一个在同一时刻才有权拥有它。 |
递归锁(Recursive lock) | 递归锁是互斥锁的一个变种。递归锁允许同一个线程在其释放前多次请求它。其他的线程则会阻塞直到锁的持有者以和请求次数相同的释放次数释放该锁。递归锁主要用于递归操作,但也可以用于多个方法间独立地请求该锁。 |
读写锁(Read-write lock) | 读写锁可以被视作共享的排他锁。该类型的锁典型地应用于大批量的操作以及被保护数据读操作频繁而写操作稀少的场景中。正常操作情况下,大量的读者可以同时访问数据。然而当线程想写数据时,它直到读者释放锁前都会阻塞,只有请求到该锁时才能进行数据更新的操作。当写入线程等待锁时,新的读者线程会阻塞直到写线程完成。系统仅支持POSIX线程使用读写锁。 |
分布式锁(Distributed lock) | 分布式锁提供进程级别的互斥访问。不像真正的互斥。分布式锁不阻塞进程的运行。它仅报告锁很忙并让进程决定如何执行。 |
自旋锁(Spin lock) | 自旋锁在条件满足前会不断地轮询获得锁。自旋锁常用于预计等待锁的时间短的多处理器系统中。在这样的前提下,轮询得到锁通常比阻塞线程更加高效,因为后者涉及到线程状态的切换和线程数据的更新。由于其轮询的特性,系统不直接提供自旋锁的实现,但你可以在特定环境中简单地实现。(译者注:已经不建议最新系统中使用OSSpinLock,了解更多信息请移步这里) |
双重检查锁(Double-checked lock) | 双重检查锁为减少持有锁带来的资源消耗,它总是在持有锁前先测试锁的临界条件。由于双重检查锁有潜在的安全问题,系统并不显式地提供支持且不鼓励使用它。 |
注意:大多数类型的锁也包含了内存屏障区,以确保进入临界区前完成先前需要完成的指令。
条件量
条件量作为另一种信号量类型以允许线程在某种条件满足时相互通知。条件量常用于预示资源的可用性或者确保任务以特定顺序执行。当线程在测试条件时,它会一直阻塞直到条件为真。同样地,直到其他线程显式地修改条件前也会保持阻塞状态。条件量与互斥锁的区别在于多个线程能够同时访问锁。条件量更像一个守门员一样,它根据某些特定的条件让不同的线程通过这道“门”。
一种可能使用条件量的情形是管理一系列挂起的事件。事件队列需要使用一个条件变量来通知队列中有等待事件的线程。如果事件到达,队列会正确地唤起条件量。如果线程仍在等待,它会因为需要将事件放入队列并处理而被唤醒。如果两个事件几乎同时进入队列,队列会通知条件量两次并唤起两个线程。
系统以几种不同的技术为条件量提供支持。条件量的实现需要仔细的代码编写,所以你需要在编写自己的代码前查看后面的示例代码。
Perform Selector
Cocoa应用有以同步形式向单个线程传递信息的便利方式。NSObject类声明了在应用活动线程上执行选择器的一些方法。这些方法使你的线程异步地传递信息并保证在目标线程上同步地执行。例如,你可能使用这种方式向应用的主线程或者指定线程传递分布式运算的结果。每次执行选择器的请求都被存储在线程run loop的事件队列中,并且请求会按照他们被收到时的顺序线性地执行。
同步消耗与性能
同步助你确保代码的正确性,但那样做确实损耗不少性能。即便在无冲突的情况下,同步工具都不是首选。锁和原子操作通常涉及到内存屏障和内核级同步过程的使用来确保代码受到正确保护。如果出现了锁的冲突,你的线程会阻塞并经历更严重的延迟。
表4-2列举了使用互斥和原子操作的近似消耗。这些数据作为从几千份样本中采集的平均值。由于考虑了线程的创建时间,互斥锁获取时间会根据处理器的负载、计算机速度、系统内存和程序内存而千差万别。
表4-2 互斥和原子操作资源消耗
指标 | 近似消耗 | 注释 |
---|---|---|
互斥锁获取时间 | 接近0.2微秒 | 这是在无冲突的情况下锁的获取时间。如果锁被另一个线程持有,获取时间还会更多。该数据由平台上(基于Intel处理器的iMac/2GHz双核处理器/1G内存/OS X 10.5系统)分析平均值和中位值测定的数据决定。 |
原子性的compare-and-swap | 接近0.05微秒 | 这是在无冲突的情况下执行compare-and-swap操作的花费时间。该数据由平台上(基于Intel处理器的iMac/2GHz双核处理器/1G内存/OS X 10.5系统)分析平均值和中位值测定的数据决定。 |
当你设计并发任务时,正确性才通常是最为重要的考虑因素,但是你也应该考虑性能因素。代码在多线程的方式下正确执行,但是却比单线程执行更慢,这就难说是一种改进了。
如果你正在对一个现有的单线程应用程序进行改造,你应该经常设置一系列关键任务的性能基线测量值。在添加附加的线程后,你应该对于相同的任务收集新的数据并比较单线程和多线程的性能差异。如果在调整代码后,线程并没有提高性能,你可能希望重新考虑你的具体实现或使用线程的方式问题。
更多关于性能和数据收集工具的信息,请看[译]性能概述。
线程安全与信号量
当遇到多线程应用,没有什么比处理信号量更让人恐惧和困惑的了。信号量是一种低等级的BSD机制以便能用于传输给信息进程或以某种方式处理信息。一些程序使用信号量来检测特定事件,比如子进程的死亡。系统会使用信号量来终止失去控制的进程以及传递其他类型的信息。
信号量的问题并不在它们做的是什么,而是应用有多个线程时它们的行为。在单线程应用中,所有的信号量处理回调都在主线程上。在多线程应用中,不依赖于任何硬件错误的(如非法指令)的信号被传递到任何一个线程的合适时机下运行。如果多个线程同时运行,信号量会被传递到系统所选的任何一个线程。换句话说,信号量可以被传递到应用中的任何线程。
在应用程序中实现信号处理回调的首要规则是避免线程处理该信号的假设。如果某个特定的线程需要处理给定的信号,则需要编写在信号到达时通知该线程的一些方法。你不能只假设线程中信号处理回调的安装会让该信号被传递到这个线程。
获取更多关于信号量和信号量回调安装的信息,请查看signal和sigaction的man帮助页面。