run loop 是与线程相关的基础架构的一部分。 它是一个事件处理循环,用于计划工作并协调传入事件的接收。 它的目的是在有工作时保持线程忙,并在没有工作时让线程进入休眠状态。 我们开发者的代码提供了用于实现 run loop 的实际循环部分的控制语句 - 换句话说,代码提供了驱动 run loop 的 while 或 for 循环。 在循环中,使用 run loop 对象运行接收事件处理代码,并调用已安装的处理程序。
run loop 从两种不同类型的源接收事件。 输入源(input sources)提供异步事件,通常是来自另一个线程或来自不同应用程序的消息。 定时器源(timer sources)提供同步事件,发生在预定时间或重复间隔。 两种类型的源都使用特定于应用程序的处理程序程序来处理到达事件。
下图显示了 run loop 和各种源的概念结构。 输入源将异步事件传递给相应的处理程序,并调用 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)退出。 计时器源将事件传递给其处理程序,但不会导致 run loop 退出。
除了处理输入源之外, run loop 还会生成有关 run loop 行为的通知。 可以使用 Core Foundation 在线程上安装 run loop 观察器。
如果输入源未处于当前监听模式,则它生成的任何事件会被 hold 住,直到 run loop 以正确模式运行。
Port-Based Sources
Cocoa 和 Core Foundation 提供 使用与端口相关的对象和函数创建基于端口的输入源 的内置支持。 例如,在 Cocoa 中,根本不必直接创建输入源,只需创建一个端口对象,并使用 NSPort 的方法将该端口添加到 run loop 中。 port 对象处理所需输入源的创建和配置。
在 Core Foundation 中,必须手动创建端口及其 run loop 源。 在这两种情况下,都使用与端口 opaque 类型(CFMachPortRef CFMessagePortRef 或 CFSocketRef)相关联的函数来创建适当的对象。
Custom Input Sources
要创建自定义输入源,必须在 Core Foundation 中使用与 CFRunLoopSourceRef opaque 类型关联的函数。 可以使用多个回调函数配置自定义输入源。 Core Foundation 在不同的点调用这些函数来配置源,处理传入事件,并在从 run loop 中删除源时拆除源。
除了基于端口的源之外, Cocoa 还定义了一个自定义输入源,允许在任何线程上执行选择器(selector)。与基于端口的源类似,在目标线程上依次执行选择器请求,从而减轻了在一个线程上运行多个方法时可能发生的许多同步问题。与基于端口的源不同,选择器源在执行后将其自身从 run loop 中移除。
在另一个线程上执行选择器时,目标线程必须具有 active run loop 。对于自定义创建的线程,这意味着要等到代码显式启动 run loop 。但是,因为主线程启动了自己的 run loop ,所以只要应用程序调用应用程序委托的 applicationDidFinishLaunching: 方法,就可以开始在该线程上发出调用。 run loop 每次通过循环处理所有排队的执行选择器调用,而不是在每次循环迭代期间处理一个。
虽然它生成基于时间的通知,但计时器不是实时的 (它有一个属性 tolerance (宽容度),标示了当时间点到后,容许有多少最大误差,目的就是为了节省资源)。 与输入源类似,定时器与 run loop 的特定模式相关联。 如果计时器未处于 run loop 当前正在监听的模式,则在使用其中一个计时器支持的模式运行 run loop 之前,它不会触发。 类似地,如果计时器在 run loop 处于执行处理程序的过程中触发,则计时器将等待直到下一次通过 run loop 来调用其处理程序。 如果 run loop 根本没有运行,则计时器永远不会触发。
可以使用 run loop 对象手动唤醒它,其他事件也可能导致 run loop 被唤醒。例如,添加另一个非基于端口的输入源会唤醒 run loop ,以便可以立即处理输入源,而不是等到其他事件发生。
Run Loop Modes
这个概念很重要, run loop mode(模式) 是要监听的输入源(input sources)和计时器(timers)的集合,以及要通知的 run loop observers 的集合。每次运行 run loop 时,都指定(显式或隐式)运行的特定模式。在 run loop 的传递过程中,仅监听与该模式关联的源并允许其传递事件。 类似地,只有与该模式相关联的 observers 被通知 run loop 的进度。 与其他模式相关联的源保持新事件,直到后续以适当模式通过循环。
在代码中,可以按名称识别模式。 Cocoa 和 Core Foundation 都定义了默认模式和几种常用模式(见下表),以及用于在代码中指定这些模式的字符串。 只需为模式名称指定自定义字符串即可定义自定义模式。 虽然为自定义模式指定的名称是任意的,但这些模式的内容不是。 必须确保将一个或多个输入源,计时器或 run loop 观察器添加到为其创建的任何模式中才有用。 (ps: 模式中要有相关源和观察器才有用)
// Listing 3-1 Creating a run loop observer - (void)threadMain { // The application uses garbage collection, so no autorelease pool is needed. NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; // Create a run loop observer and attach it to the 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); } // Create and schedule the timer. [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES]; NSInteger loopCount = 10; do { // Run the run loop 10 times to let the timer fire. [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; loopCount--; } while (loopCount); }
为长期存在的线程配置 run loop 时,最好添加至少一个输入源来接收消息。 虽然可以连接一个定时器进入 run loop ,但一旦定时器触发,它通常会失效,这会导致运行循环退出。 重复计时器可以使 run loop 运行更长的时间,但是会涉及定期触发计时器以唤醒线程,这实际上是另一种形式的轮询。 相比之下,输入源会等待事件发生,让线程保持睡眠状态。
Starting the Run Loop
只用次级线程才需要启动 run loop 。 run loop 必须至少有一个输入源或者计时器才能进行监听,如果没有,则会立即退出。有以下几种方法可以启动 run loop
无条件 (run)
设置时限 (runUntilDate:)
在特定模式下 (runMode:beforeDate:)
无条件是最简单的,但也是最不可取的。它让 run loop 置于永久循环中,使得我们开发者无法控制它。我们可以添加和删除输入源或者计时器,但是停止它的唯一方法是杀掉他。并且它也没法在自定义模式下运行。
最好设置时限运行 run loop ,它将一直运行,直到事件到达或分配的时间到期。如果事件到达,则将该事件分派给处理程序进行处理,然后退出 run loop 。可以重新启动它以处理下一个事件。如果指定的时间到期了,只需重新启动它或使用时间进行任何所需的内务(housekeeping)处理。
可以使用特定模式运行 run loop 。它与设置时限不是互斥的,模式限制将事件传递到 run loop 的源类型。
下面的代码展示了线程主入口程序的框架,显示了 run loop 的基本结构。 实质上,将输入源和计时器添加到 run loop 中,然后重复调用其中一个程序以启动 run loop 。 每次 run loop 程序返回时,都会检查是否出现了可能需要退出该线程的任何条件。 该示例使用 Core Foundation 运行 run loop ,以便它可以检查返回结果并确定运行循环退出的原因。 如果使用 Cocoa 并且不需要检查返回值,也可以使用 NSRunLoop 类的方法以类似的方式运行 run loop (后面 Listing 3-14 有代码)。
// Listing 3-2 Running a run loop - (void)skeletonThreadMain { // Set up an autorelease pool here if not using garbage collection. BOOL done = NO; // Add your sources or timers to the run loop and do any other setup. do { // Start the run loop but return after each source is handled. SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES); // If a source explicitly stopped the run loop, or if there are no // sources or timers, go ahead and exit. if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) done = YES; // Check for any other exit conditions here and set the // done variable as needed. } while (!done); // Clean up code here. Be sure to release any allocated autorelease pools. }
可以递归地运行 run loop 。 换句话说,可以调用 CFRunLoopRun , CFRunLoopRunInMode 或任何 NSRunLoop 方法,以便从输入源或计时器的处理程序程序中启动 run loop 。 执行此操作时,可以使用任何要运行嵌套 run loop 的模式,包括外部 run loop 使用的模式。
Exiting the Run Loop
在处理事件之前,有两种方法可以使 run loop 退出:
使用超时值
告诉 run loop 停止
推荐使用超时值,因为我们可以管理 run loop 。它能让 run loop 完成所有的正常处理,包括给 observer 发送通知。
使用 CFRunLoopStop 函数可以手动停止 run loop。 run loop 发出剩下的通知后退出。用这个方法可以停止无条件启动的 run loop 。
虽然删除 run loop 的输入源和定时器也可能导致运行循环退出,但这不是停止 run loop 的可靠方法。一些系统程序将输入源添加到 run loop 以处理所需的事件。我们可能不知道它们,所以删除的时候就会漏掉它们。
Thread Safety and Run Loop Objects
Core Foundation 中的函数通常是线程安全的,可以从任何线程调用。但是,如果要执行更改 run loop 配置的操作,尽可能从拥有 run loop 的线程执行此操作。
Cocoa NSRunLoop 类不像 Core Foundation 对应的那样具有内在的线程安全性。 如果使用 NSRunLoop 类来修改 run loop ,必须在拥有该 run loop 的同一线程执行此操作。 将输入源或计时器添加到属于不同线程的 run loop 可能会导致代码崩溃或以意外方式运行。 (ps: 在拥有 run loop 的线程中做相关更改 run loop 的操作。)
Configuring Run Loop Sources
Defining a Custom Input Source
创建自定义输入源涉及到以下定义:
The information you want your input source to process. (希望输入源处理的信息)
A scheduler routine to let interested clients know how to contact your input source. (让感兴趣的客户端知道如何联系输入源的 scheduler 程序)
A handler routine to perform requests sent by any clients. (执行 客户端发送的请求的 处理程序)
A cancellation routine to invalidate your input source. (使输入源无效的取消程序)
下图显示了自定义输入源的示例配置。主线程维护对输入源的引用,以及该输入源的自定义命令缓冲区(custom command buffer)以及安装输入源的 run loop 。当主线程有一个任务,想要传递给工作线程时,它会向命令缓冲区发布一个命令以及工作线程启动任务所需的任何信息。 (因为主线程和工作线程的输入源都可以访问命令缓冲区,所以必须同步该访问。) 一旦命令发布,主线程就会发出信号输入源并唤醒工作线程的 run loop 。收到唤醒命令后, run loop 调用输入源的处理程序,该处理程序处理命令缓冲区中的命令。
// Listing 3-9 Waking up the run loop - (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop { CFRunLoopSourceSignal(runLoopSource); CFRunLoopWakeUp(runloop); }
Configuring Timer Sources
要创建计时器源,所要做的就是创建一个计时器对象并在 run loop 上安排(schedule)它。在 Cocoa 中,使用 NSTimer 创建计时器对象,在 Core Foundation 中使用 CFRunLoopTimerRef opaque 类型。在内部, NSTimer 类只是 Core Foundation 的扩展,它提供了一些便利功能。用以下方法可以创建
这些方法创建计时器并将其以默认模式(NSDefaultRunLoopMode)添加到当前线程的 run loop 中。如果需要,还可以手动调度计时器,方法是创建 NSTimer 对象,然后使用 NSRunLoop 的 addTimer:forMode: 方法将其添加到 run loop 中。这两种技术基本上都是一样的,但是可以对计时器的配置进行不同程度的控制。例如,如果创建计时器并手动将其添加到 run loop ,则可以使用默认模式以外的模式执行此操作。下面的代码显示了如何使用这两种技术创建计时器。第一个计时器的初始延迟为 1 秒,但之后每 0.1 秒定时触发一次。第二个计时器在最初的 0.2秒延迟后开始触发,然后每 0.2 秒触发一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// Listing 3-10 Creating and scheduling timers using 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];
下面的代码展示了使用 Core Foundation 函数配置计时器所需的代码。 虽然此示例未在上下文结构中传递任何用户定义的信息,但可以使用此结构传递计时器所需的任何自定义数据。
1 2 3 4 5 6 7 8
// Listing 3-11 Creating and scheduling a timer using 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);
Configuring a Port-Based Input Source
Cocoa 和 Core Foundation 都提供了基于端口的对象,用于线程之间或进程之间的通信。 以下部分介绍如何使用多种不同类型的端口设置端口通信。
Configuring an NSMachPort Object
要与 NSMachPort 对象建立本地连接,请创建端口对象并将其添加到主线程的 run loop 中。 启动次级线程时,将同一对象传递给线程的入口函数。 次级线程可以使用相同的对象将消息发送回主线程。
Implementing the Main Thread Code
下面的代码展示了启动次级工作线程的主要代码。因为 Cocoa 框架执行许多配置端口和 run loop 的干预步骤,所以 launchThread 方法明显比其 Core Foundation 等效方法要短(见后面 Listing 3-17); 然而,两者的行为几乎完全相同。 一个区别是,该方法不是直接向工作线程发送本地端口的名称,而是直接发送 NSPort 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Listing 3-12 Main thread launch method - (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]; } }
#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. } }
// Listing 3-14 Launching the worker thread using Mach ports
+(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]; }
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];
Configuring a Port-Based Input Source in Core Foundation
本节介绍如何使用 Core Foundation 在应用程序的主线程和工作线程之间建立双向通信通道。
// Listing 3-17 Attaching a Core Foundation message port to a new thread
#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)); }
安装端口并启动线程后,主线程可以在等待线程 check in 时继续其常规执行。当 check-in 消息到达时,它将被分派到主线程的 MainThreadResponseHandler 函数,如下面代码所示,此函数提取工作线程的端口名称,并为将来的通信创建管道。
#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; }
配置主线程后,剩下的唯一事情就是新创建的工作线程创建自己的端口并 check in 。下面代码展示了工作线程的入口函数。 该函数提取主线程的端口名称,并使用它创建一个返回主线程的远程连接。 然后,该函数为自己创建一个本地端口,在该线程的 run loop 上安装该端口,并向包含本地端口名称的主线程发送一个 check-in 消息。
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 函数处理。 该函数的实现取决于线程的工作类型,这里没有显示。