个性化阅读
专注于IT技术分析

Objective-C线程技术开发详解

点击下载

线程是使在单个应用程序中同时执行多个代码路径成为可能的几种技术之一。虽然较新的技术,如operation objects和Grand Central Dispatch (GCD),为实现并发提供了更现代、更高效的基础设施,但OS X和iOS也提供了创建和管理线程的接口。

本文介绍了OS X中可用的线程包,并向你展示了如何使用它们。本文还描述了为支持应用程序内多线程代码的线程化和同步而提供的相关技术。

重要提示:如果你正在开发一个新的应用程序,建议你研究用于实现并发性的其他OS X技术。如果你还不熟悉实现线程应用程序所需的设计技术,则尤其如此。这些替代技术简化了实现并发执行路径的工作量,并提供了比传统线程更好的性能。有关这些技术的信息,请参阅并发编程指南。

本文大纲

本文件有以下章节和附录:

  • 关于线程编程,介绍了线程的概念及其在应用程序设计中的作用。
  • 线程管理,提供有关OS X中的线程技术以及如何使用它们的信息。
  • Run Loops,提供有关如何在辅助线程中管理事件处理循环的信息。
  • 线程同步,描述同步问题和用于防止多线程破坏数据或使程序崩溃的工具。
  • 线程安全摘要,提供了OS X和iOS及其一些关键框架固有的线程安全的高级摘要。

关于线程编程

多年来,计算机的最高性能在很大程度上受到位于计算机核心的单个微处理器的速度的限制。然而,随着单个处理器的速度开始达到其实际极限,芯片制造商转而采用多核设计,使计算机有机会同时执行多个任务。尽管OS X在任何时候都可以利用这些核心来执行与系统相关的任务,但是你自己的应用程序也可以通过线程来利用它们。

什么是线程?

线程是在应用程序内部实现多条执行路径的一种相对轻量级的方法。在系统级,程序并行运行,系统根据每个程序的需要和其他程序的需要分配执行时间。然而,在每个程序中都存在一个或多个执行线程,这些线程可用于同时或几乎同时执行不同的任务。系统本身实际上管理这些执行线程,调度它们在可用的核心上运行,并在需要时先发制人地中断它们以允许其他线程运行。

从技术角度来看,线程是管理代码执行所需的内核级和应用程序级数据结构的组合。内核级结构协调向线程调度事件和在一个可用内核上抢占线程的调度。应用程序级结构包括用于存储函数调用的调用堆栈,以及应用程序管理和操作线程的属性和状态所需的结构。

在非并发应用程序中,只有一个执行线程。该线程从应用程序的主例程开始并结束,然后逐个分支到不同的方法或函数,以实现应用程序的整体行为。相反,支持并发的应用程序从一个线程开始,并根据需要添加更多线程来创建额外的执行路径。每个新路径都有自己的自定义start例程,该例程独立于应用程序主例程中的代码运行。在应用程序中拥有多个线程提供了两个非常重要的潜在优势:

  • 多线程可以提高应用程序的响应能力。
  • 多线程可以提高应用程序在多核系统上的实时性能。

如果你的应用程序只有一个线程,那么这个线程必须完成所有任务。它必须响应事件,更新应用程序的窗口,并执行实现应用程序行为所需的所有计算。只有一个线程的问题是它一次只能做一件事。那么当你的一个计算需要很长时间才能完成时会发生什么呢?当代码忙于计算所需的值时,应用程序将停止响应用户事件并更新其窗口。如果这种行为持续足够长的时间,用户可能会认为你的应用程序被挂起,并试图强制退出它。但是,如果你将自定义计算转移到单独的线程上,则应用程序的主线程可以自由地以更及时的方式响应用户交互。

随着多核计算机的普及,线程为某些类型的应用程序提供了一种提高性能的方法。执行不同任务的线程可以在不同的处理器内核上同时执行,这使得应用程序可以在给定的时间内增加工作量。

当然,线程不是解决应用程序性能问题的万能药。除了线程提供的好处之外,还有一些潜在的问题。在应用程序中有多个执行路径会给代码增加相当多的复杂性。每个线程必须与其他线程协调其操作,以防止它破坏应用程序的状态信息。因为单个应用程序中的线程共享相同的内存空间,所以它们可以访问所有相同的数据结构。如果两个线程试图同时操作相同的数据结构,一个线程可能会以破坏结果数据结构的方式覆盖另一个线程的更改。即使有适当的保护,你仍然必须注意编译器优化,因为它会在你的代码中引入细微的(而不是很细微的)bug。

线程的术语

在深入讨论线程及其支持技术之前,有必要定义一些基本术语。

如果你熟悉UNIX系统,你可能会发现本文档对术语“任务”的使用有所不同。在UNIX系统中,“任务”一词有时用来指正在运行的进程。

本文件采用下列术语:

  • 术语“线程”用于指代码的独立执行路径。
  • 进程这个术语是用来指一个正在运行的可执行程序,它可以包含多个线程。
  • 术语任务是指需要执行的工作的抽象概念。

线程的替代技术

自己创建线程的一个问题是它们会给代码增加不确定性。线程是在应用程序中支持并发性的一种相对低级和复杂的方式。如果你不完全理解设计选择的含义,那么很容易遇到同步或计时问题,其严重程度从细微的行为变化到应用程序崩溃和用户数据损坏不等。

另一个需要考虑的因素是是否需要线程或并发。线程解决了如何在同一进程内并发执行多个代码路径的具体问题。但是,在某些情况下,你所做的工作量并不保证并发性。线程会给进程带来大量的开销,包括内存消耗和CPU时间。你可能会发现,对于预期的任务来说,这种开销太大了,或者其他选项更容易实现。

表1-1列出了一些线程的替代方案。此表包括线程的替换技术(如操作对象和GCD)和旨在有效使用现有单线程的替代技术。

表1-1线程的替代技术

技术解释
Operation objects在OS X v10.5中引入的操作对象是一个任务的包装器,该任务通常在辅助线程上执行。这个包装器隐藏了执行任务的线程管理方面的内容,让你可以自由地关注任务本身。你通常将这些对象与操作队列对象一起使用,操作队列对象实际上管理操作对象在一个或多个线程上的执行。
有关如何使用操作对象的更多信息,请参见并发编程指南。
Grand Central Dispatch (GCD)Grand Central Dispatch是在Mac OS x v10.6中引入的,它是线程的另一种替代方法,可以让你专注于需要执行的任务,而不是线程管理。使用GCD,你可以定义要执行的任务,并将其添加到工作队列中,工作队列将在适当的线程上处理任务的调度。工作队列考虑可用内核的数量和当前的负载,以便比使用线程更有效地执行任务。
有关如何使用GCD和工作队列的信息,请参阅并发编程指南
Idle-time notifications对于相对较短且优先级非常低的任务,空闲时间通知允许你在应用程序不那么忙的时候执行该任务。Cocoa使用NSNotificationQueue对象为空闲时间通知提供支持。要请求空闲时间通知,请使用NSPostWhenIdle选项将通知发送到默认的NSNotificationQueue对象。队列将延迟通知对象的传递,直到运行循环变为空闲。有关更多信息,请参见通知编程主题。
Asynchronous functions系统接口包括许多为你提供自动并发的异步功能。这些api可能使用系统守护进程和进程,或者创建自定义线程来执行它们的任务并将结果返回给你。(实际的实现是不相关的,因为它与你的代码是分离的。)在设计应用程序时,寻找提供异步行为的函数,并考虑使用它们而不是在自定义线程上使用等效的同步函数。
Timers你可以在应用程序的主线程上使用计时器来执行周期性任务,这些任务太过琐碎,不需要线程,但仍然需要定期维护。有关计时器的信息,请参阅计时器源。
Separate processes尽管比线程更重量级,但在任务与应用程序无关的情况下,创建单独的进程可能会很有用。如果任务需要大量内存或必须使用根特权执行,则可以使用进程。例如,在32位应用程序向用户显示结果时,可以使用64位服务器进程来计算大型数据集。

线程支持

如果你有使用线程的现有代码,OS X和iOS提供了几种在应用程序中创建线程的技术。此外,这两个系统还支持管理和同步需要在这些线程上完成的工作。以下部分描述了在OS X和iOS中使用线程时需要注意的一些关键技术。

线程包

尽管线程的底层实现机制是Mach线程,但是你很少(如果有的话)使用Mach级别的线程。相反,你通常使用更方便的POSIX API或它的一个衍生物。但是,Mach实现确实提供了所有线程的基本特性,包括抢占式执行模型和调度线程的能力,使它们彼此独立。

清单2-2列出了可以在应用程序中使用的线程技术。

表1-2线程技术

技术解释
Cocoa threadsCocoa使用NSThread类实现线程。Cocoa还提供了NSObject上的方法,用于生成新线程并在已经运行的线程上执行代码。有关更多信息,请参见使用NSThread和使用NSObject派生线程。
POSIX threadsPOSIX线程为创建线程提供了一个基于c的接口。如果你不编写Cocoa应用程序,这是创建线程的最佳选择。POSIX接口使用起来相对简单,并且为配置线程提供了足够的灵活性。有关更多信息,请参见使用POSIX线程
Multiprocessing Services多处理服务是一个遗留的基于c的接口,用于从旧版本Mac OS转换过来的应用程序。这种技术只在OS X中可用,应该避免任何新的开发。相反,你应该使用NSThread类或POSIX线程。如果需要有关此技术的更多信息,请参阅多处理服务编程指南。

在应用程序级别,所有线程的行为本质上与其他平台上的行为相同。启动一个线程后,该线程以三种主要状态之一运行:运行、就绪或阻塞。如果一个线程当前没有运行,它要么被阻塞,等待输入,要么准备运行,但还没有计划这样做。线程继续在这些状态之间来回移动,直到最后退出并移动到终止状态。

创建新线程时,必须为该线程指定一个入口点函数(或Cocoa线程中的入口点方法)。这个入口点函数构成了你希望在线程上运行的代码。当函数返回时,或显式终止线程时,线程将永久停止并由系统回收。由于创建线程在内存和时间方面相对昂贵,因此建议你的入口点函数执行大量的工作,或者设置一个run循环,以允许执行重复的工作。

有关可用线程技术以及如何使用它们的更多信息,请参见线程管理。

循环运行run loop

运行循环是一种用于管理线程上异步到达的事件的基础结构。运行循环通过监视线程的一个或多个事件源来工作。当事件到达时,系统唤醒线程并将事件分派给运行循环,然后运行循环将事件分派给你指定的处理程序。如果没有事件出现并准备好处理,run循环将线程置为睡眠状态。

你不需要对创建的任何线程使用run循环,但是这样做可以为用户提供更好的体验。运行循环使创建使用最少资源的长生命周期线程成为可能。因为运行循环在无事可做时将线程置为睡眠状态,所以它消除了轮询的需要,而轮询会浪费CPU周期,并阻止处理器本身进入睡眠状态并节省电能。

要配置运行循环,你所要做的就是启动线程,获得对运行循环对象的引用,安装事件处理程序,并告诉运行循环运行。OS X提供的基础结构自动为你处理主线程的运行循环配置。但是,如果你计划创建长生命周期的辅助线程,则必须自己为这些线程配置run循环。

有关运行循环的详细信息和如何使用它们的示例在运行循环中提供。

同步工具

线程编程的危害之一是多个线程之间的资源争用。如果多个线程试图同时使用或修改相同的资源,可能会出现问题。缓解这个问题的一种方法是完全消除共享资源,并确保每个线程都有自己的一组要操作的资源。但是,当不能维护完全独立的资源时,你可能必须使用锁、条件、原子操作和其他技术来同步对资源的访问。

锁为一次只能由一个线程执行的代码提供了一种强力保护形式。最常见的锁类型是互斥锁,也称为互斥锁。当一个线程试图获取当前由另一个线程持有的互斥锁时,它会阻塞,直到锁被另一个线程释放。一些系统框架提供了对互斥锁的支持,尽管它们都基于相同的底层技术。另外,Cocoa提供了互斥锁的几个变体来支持不同类型的行为,比如递归。有关可用锁类型的更多信息,请参见锁。

除了锁之外,系统还提供对条件的支持,以确保应用程序中任务的正确排序。条件充当守门人,阻塞给定的线程,直到它所代表的条件变为真。当这种情况发生时,条件释放线程并允许它继续。POSIX层和基础框架都提供了对条件的直接支持。(如果使用操作对象,可以配置操作对象之间的依赖关系,以对任务的执行进行排序,这与条件提供的行为非常相似。)

虽然锁和条件在并发设计中非常常见,但原子操作是保护和同步数据访问的另一种方法。在可以对标量数据类型执行数学或逻辑操作的情况下,原子操作为锁提供了一种轻量级的替代方法。原子操作使用特殊的硬件指令来确保在其他线程有机会访问变量之前完成对变量的修改。

有关可用同步工具的更多信息,请参见同步工具。

线程通信

虽然一个好的设计会最小化所需的通信量,但是在某些情况下,线程之间的通信是必要的。(线程的任务是为应用程序工作,但是如果该任务的结果从未被使用过,那么它有什么用呢?)线程可能需要处理新的作业请求或将它们的进展报告给应用程序的主线程。在这些情况下,你需要一种方法将信息从一个线程传递到另一个线程。幸运的是,线程共享相同的进程空间这一事实意味着你有许多通信选项。

线程之间的通信有很多方式,每种方式都有其优缺点。配置线程本地存储列出了你可以在OS x中使用的最常见的通信机制(除了消息队列和Cocoa分布式对象之外,这些技术在iOS中也可用)。本表中列出的技术是按照增加复杂性的顺序排列的。

表1-3线程通信机制

机制解释
直接传递Cocoa应用程序支持直接在其他线程上执行选择器。这个功能意味着一个线程可以在任何其他线程上执行一个方法。因为它们是在目标线程的上下文中执行的,所以以这种方式发送的消息会在该线程上自动序列化。有关输入源的信息,请参阅Cocoa执行选择器源。
全局变量、共享内存和对象在两个线程之间通信信息的另一种简单方法是使用全局变量、共享对象或共享内存块。尽管共享变量快速且简单,但它们也比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。如果不这样做,可能会导致竞争条件、损坏的数据或崩溃。
条件条件是一种同步工具,你可以使用它来控制线程何时执行代码的特定部分。你可以将条件视为“门控”,仅在满足所述条件时才允许线程运行。有关如何使用条件的信息,请参见使用条件。
运行循环源自定义运行循环源是为在线程上接收特定于应用程序的消息而设置的源。因为它们是事件驱动的,所以当无事可做时,运行循环源会让线程自动休眠,从而提高线程的效率。有关运行循环和运行循环源的信息,请参见运行循环。
端口和套接字基于端口的通信是两个线程之间通信的一种更复杂的方式,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(如其他流程和服务)通信。为了提高效率,端口是使用运行循环源实现的,所以当没有数据等待端口时,线程就会休眠。有关运行循环和基于端口的输入源的信息,请参见运行循环。
消息队列遗留的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出的数据。尽管消息队列简单方便,但它们不如其他一些通信技术有效。有关如何使用消息队列的更多信息,请参阅多处理服务编程指南。
Cocoa分布式对象分布式对象是一种Cocoa技术,它提供了基于端口的通信的高级实现。尽管可以将此技术用于线程间通信,但由于它会带来大量开销,因此不建议这样做。分布式对象更适合与其他进程通信,因为在这些进程之间运行的开销已经很高了。有关更多信息,请参见分布式对象编程主题。

设计技巧

以下部分提供了一些指导原则,帮助你以确保代码正确性的方式实现线程。其中一些指导原则还提供了使用你自己的线程代码实现更好性能的技巧。与任何性能技巧一样,你应该始终在对代码进行更改之前、期间和之后收集相关的性能统计信息。

避免显式地创建线程

手工编写线程创建代码非常繁琐,而且可能容易出错,你应该尽可能避免这样做。OS X和iOS通过其他api提供了对并发性的隐式支持。与其自己创建一个线程,不如考虑使用异步api、GCD或操作对象来完成这项工作。这些技术在幕后为你完成与线程相关的工作,并且保证能够正确地完成这些工作。此外,GCD和operation对象等技术的设计目的是,通过根据当前系统负载调整活动线程的数量,比你自己的代码更有效地管理线程。有关GCD和操作对象的更多信息,请参见并发编程指南。

保持你的线程相当繁忙

如果你决定手动创建和管理线程,请记住线程会消耗宝贵的系统资源。你应该尽最大努力确保分配给线程的任何任务都具有合理的长生命周期和生产力。同时,你不应该害怕终止那些大部分时间处于空闲状态的线程。线程使用大量的内存,其中一些是连接的,因此释放空闲线程不仅有助于减少应用程序的内存占用,还可以释放更多的物理内存供其他系统进程使用。

重要提示:在开始终止空闲线程之前,应该始终记录一组应用程序当前性能的基线测量值。在尝试了更改之后,进行额外的度量,以验证更改实际上提高了性能,而不是损害性能。

避免共享数据结构

避免与线程相关的资源冲突的最简单和最简单的方法是给程序中的每个线程它自己需要的任何数据的副本。当你最小化线程之间的通信和资源争用时,并行代码工作得最好。

创建多线程应用程序很困难。即使你非常小心并在代码中所有正确的连接点处锁定共享数据结构,你的代码在语义上仍然是不安全的。例如,如果你的代码期望按照特定的顺序修改共享数据结构,那么它可能会遇到问题。将你的代码更改为基于事务的模型来进行补偿,可能会抵消拥有多个线程的性能优势。从一开始就消除资源争用通常会导致更简单的设计和更好的性能。

线程和用户界面

如果你的应用程序具有图形用户界面,建议你接收与用户相关的事件,并从应用程序的主线程发起接口更新。这种方法有助于避免与处理用户事件和绘制窗口内容相关的同步问题。一些框架(如Cocoa)通常需要这种行为,但即使那些不需要,将这种行为保持在主线程上也可以简化管理用户界面的逻辑。

有几个值得注意的例外是,从其他线程执行图形化操作是有利的。例如,可以使用辅助线程创建和处理图像并执行其他与图像相关的计算。为这些操作使用辅助线程可以极大地提高性能。如果你不确定某个特定的图形化操作,请计划在主线程中执行。

有关Cocoa线程安全的更多信息,请参见线程安全摘要。有关在Cocoa中绘图的更多信息,请参见Cocoa绘图指南。

在退出时注意线程行为

一个进程运行到所有非分离的线程都退出为止。默认情况下,只有应用程序的主线程被创建为非分离的,但是你也可以通过这种方式创建其他线程。当用户退出应用程序时,通常认为立即终止所有分离的线程是适当的行为,因为分离的线程所做的工作是可选的。但是,如果你的应用程序正在使用后台线程将数据保存到磁盘或执行其他关键工作,则可能希望将这些线程创建为非分离的,以防止在应用程序退出时丢失数据。

创建非分离线程(也称为可接合线程)需要你进行额外的工作。因为大多数高级线程技术在默认情况下都不创建可接合线程,所以你可能必须使用POSIX API来创建线程。此外,必须将代码添加到应用程序的主线程中,以便在非分离线程最终退出时加入它们。有关创建可接合线程的信息,请参见设置线程的分离状态。

如果你正在编写一个Cocoa应用程序,你也可以使用applicationShouldTerminate: 代理方法来延迟应用程序的终止,直到稍后的时间或者完全取消它。当延迟终止时,你的应用程序将需要等待,直到任何关键线程完成其任务,然后调用replyToApplicationShouldTerminate:方法。有关这些方法的更多信息,请参见NSApplication类引用。

处理异常

异常处理机制依赖于当前调用堆栈,以在抛出异常时执行任何必要的清理。由于每个线程都有自己的调用堆栈,因此每个线程都负责捕获自己的异常。在辅助线程中捕获异常失败与在主线程中捕获异常失败相同:拥有的进程终止。你不能将未捕获的异常抛出到其他线程进行处理。

如果需要将当前线程中的异常情况通知另一个线程(例如主线程),则应该捕获异常并向另一个线程发送一条消息,指示发生了什么。根据你的模型和尝试做的事情,捕获异常的线程可以继续处理(如果可能的话),等待指令,或者简单地退出。

注意:在Cocoa中,NSException对象是一个自包含的对象,一旦它被捕获,就可以从一个线程传递到另一个线程。

在某些情况下,可能会自动为你创建一个异常处理程序。例如,Objective-C中的@synchronized指令包含一个隐式异常处理程序。

干净地终止线程

线程退出的最佳方式是让它到达主入口点例程的末端。虽然有一些函数可以立即终止线程,但是这些函数只能作为最后的手段使用。在线程到达它的自然端点之前终止它可以防止线程在自身结束后进行清理。如果线程已经分配了内存、打开了文件或获得了其他类型的资源,你的代码可能无法回收这些资源,从而导致内存泄漏或其他潜在问题。

有关退出线程的正确方式的更多信息,请参见终止线程。

库中的线程安全

尽管应用程序开发人员可以控制应用程序是否使用多个线程执行,但是库开发人员不能。在开发库时,你必须假设调用应用程序是多线程的,或者可以在任何时候切换到多线程。因此,你应该始终对代码的关键部分使用锁。

对于库开发人员来说,仅在应用程序变成多线程时才创建锁是不明智的。如果你需要在某个时候锁定代码,请在使用库的早期创建锁对象,最好是在某种显式调用中初始化库。虽然你也可以使用静态库初始化函数来创建这样的锁,但是只有在没有其他方法时才尝试这样做。初始化函数的执行会增加加载库所需的时间,并可能对性能产生负面影响。

注意:始终记住要平衡调用来锁定和解锁你的库中的互斥锁。你还应该记住锁定库数据结构,而不是依赖调用代码来提供线程安全的环境。

如果你正在开发一个Cocoa库,如果你希望在应用程序变成多线程时得到通知,那么你可以注册为NSWillBecomeMultiThreadedNotification的观察者。但是,你不应该依赖于接收这个通知,因为它可能在调用库代码之前就被发送了。

线程管理

OS X或iOS中的每个进程(应用程序)由一个或多个线程组成,每个线程表示通过应用程序代码执行的单一路径。每个应用程序都由一个线程开始,该线程运行应用程序的主函数。应用程序可以产生额外的线程,每个线程执行特定函数的代码。

当应用程序生成一个新线程时,该线程将成为应用程序进程空间内的一个独立实体。每个线程都有自己的执行堆,并由内核分别为运行时调度。一个线程可以与其他线程和其他进程通信,执行I/O操作,以及执行你可能需要它执行的任何其他操作。但是,由于它们位于相同的进程空间中,单个应用程序中的所有线程都共享相同的虚拟内存空间,并且具有与进程本身相同的访问权限。

这里概述OS X和iOS中可用的线程技术,以及如何在应用程序中使用这些技术的示例。

线程的成本

在内存使用和性能方面,线程对程序(和系统)有实际的成本。每个线程都需要在内核内存空间和程序的内存空间中分配内存。管理线程和协调其调度所需的核心结构使用有线内存存储在内核中。线程的堆栈空间和每个线程的数据存储在程序的内存空间中。大多数这些结构都是在你第一次创建线程时创建并初始化的——由于需要与内核进行交互,因此线程进程的开销可能比较大。

表2-1量化了在应用程序中创建新用户级线程的大致成本。其中一些开销是可配置的,比如为辅助线程分配的堆栈空间量。创建线程的时间成本是一个粗略的近似值,应该仅用于相互之间的相对比较。根据处理器负载、计算机的速度以及可用系统和程序内存的数量,线程创建时间可能会有很大差异。

表2-1线程创建开销

条目近似开销解释
内核数据结构约为 1 KB该内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此不能分页到磁盘。
栈空间512kb(辅助线程)8mb (OS X主线程)1mb (iOS主线程)辅助线程允许的最小堆大小为16 KB,并且堆大小必须是4 KB的倍数。这个内存的空间在线程创建时保留在进程空间中,但是直到需要时才会创建与该内存相关的实际页面。
创建时间约90微秒此值反映创建线程的初始调用与线程的入口点例程开始执行之间的时间。这些数据是通过分析基于intel的iMac在线程创建过程中生成的平均值和中位数得出的,该iMac使用2ghz双核处理器和1gb RAM运行OS X v10.5。

注意:由于它们的底层内核支持,操作对象通常可以更快地创建线程。它们不是每次都从头开始创建线程,而是使用已经驻留在内核中的线程池来节省分配时间。有关使用操作对象的更多信息,请参见并发编程指南。

编写线程代码时要考虑的另一个成本是生产成本。设计线程化的应用程序有时需要对组织应用程序数据结构的方式进行基本的更改。为了避免使用同步,进行这些更改可能是必要的,而同步本身可能会给设计糟糕的应用程序带来巨大的性能损失。设计这些数据结构和调试线程化代码中的问题可能会增加开发线程化应用程序所需的时间。但是,如果你的线程在等待锁上花费了太多时间,或者什么都不做,那么避免这些开销可能会在运行时造成更大的问题。

创建一个线程

创建低级线程相对简单。在所有情况下,必须有一个函数或方法作为线程的主要入口点,并且必须使用一个可用的线程例程来启动线程。下面几节将介绍更常用的线程技术的基本创建过程。使用这些技术创建的线程将继承一组默认属性,这些属性由你使用的技术决定。有关如何配置线程的信息,请参见配置线程属性。

使用NSThread

有两种方法来创建一个线程使用NSThread类:

  • 使用detachNewThreadSelector:toTarget:withObject: 类方法方法生成新线程。
  • 创建一个新的NSThread对象并调用它的start方法。(仅在iOS和OS X v10.5及更高版本中支持。)

这两种技术都会在应用程序中创建一个分离的线程。分离的线程意味着当线程退出时,系统将自动回收该线程的资源。这还意味着你的代码以后不必显式地与线程连接。

因为在所有版本的OS X中都支持detachNewThreadSelector:toTarget:withObject:方法,所以在使用线程的现有Cocoa应用程序中经常可以找到它。要分离一个新线程,只需提供方法的名称(指定为选择器),将其用作线程的入口点、定义该方法的对象,以及在启动时希望传递给线程的任何数据。下面的示例显示了此方法的基本调用,它使用当前对象的自定义方法生成线程。

[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];

在OS X v10.5之前,你主要使用NSThread类来派生线程。虽然你可以获取一个NSThread对象并访问一些线程属性,但你只能在线程运行后从线程本身进行。在OS X v10.5中,添加了对创建NSThread对象的支持,而无需立即生成相应的新线程。(iOS系统也提供这种支持。)这种支持使得在启动线程之前获取和设置各种线程属性成为可能。它还使以后使用线程对象来引用正在运行的线程成为可能。

在OS X v10.5及以后版本中初始化NSThread对象的简单方法是使用initWithTarget:selector:object:方法。这个方法获取与detachNewThreadSelector:toTarget:withObject:方法完全相同的信息,并使用它初始化一个新的NSThread实例。但是,它不会启动线程。要启动线程,可以显式调用线程对象的start方法,如下例所示:

NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(myThreadMainMethod:)
                                        object:nil];
[myThread start];  // Actually create the thread

注意:使用initWithTarget:selector:object: 方法的另一种方法是子类化NSThread并覆盖它的主方法。你将使用此方法的重写版本来实现线程的主入口点。有关更多信息,请参见NSThread类引用中的子类注释。

如果你有一个NSThread对象,它的线程当前正在运行,你可以发送消息给那个线程的一种方式是使用performSelector:onThread:withObject:waitUntilDone:方法在你的应用程序中几乎任何对象。OS X v10.5中引入了对在线程(主线程之外)上执行选择器的支持,这是线程之间进行通信的一种方便方式。(iOS系统也提供这种支持。)使用此技术发送的消息由其他线程直接执行,作为其正常运行循环处理的一部分。(当然,这意味着目标线程必须在其运行循环中运行;见循环运行。)当你以这种方式通信时,可能仍然需要某种形式的同步,但是这比在线程之间设置通信端口要简单。

注意:虽然对于线程之间的偶尔通信很好,但是对于线程之间的时间关键或频繁通信,你不应该使用performSelector:onThread:withObject:waitUntilDone:方法。

有关其他线程通信选项的列表,请参见设置线程的分离状态。

使用POSIX线程

OS X和iOS为使用POSIX线程API创建线程提供了基于c的支持。这种技术实际上可以用于任何类型的应用程序(包括Cocoa和Cocoa Touch应用程序),如果你正在为多个平台编写软件,这种技术可能会更方便。你用来创建线程的POSIX例程被适当地称为pthread_create。

清单2-1显示了使用POSIX调用创建线程的两个自定义函数。LaunchThread函数创建一个新线程,其主例程在posixthreadmain例程函数中实现。因为POSIX默认情况下创建的线程是可接合的,所以本例更改线程的属性以创建一个分离的线程。将线程标记为分离线程,可以让系统在线程退出时立即回收该线程的资源。

清单2-1在C中创建一个线程

#include <assert.h>
#include <pthread.h>
 
void* PosixThreadMainRoutine(void* data)
{
    // Do some work here.
 
    return NULL;
}
 
void LaunchThread()
{
    // Create the thread using POSIX routines.
    pthread_attr_t  attr;
    pthread_t       posixThreadID;
    int             returnVal;
 
    returnVal = pthread_attr_init(&attr);
    assert(!returnVal);
    returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    assert(!returnVal);
 
    int     threadError = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL);
 
    returnVal = pthread_attr_destroy(&attr);
    assert(!returnVal);
    if (threadError != 0)
    {
         // Report an error.
    }
}

如果你将上述清单中的代码添加到一个源文件并调用LaunchThread函数,它将在你的应用程序中创建一个新的分离线程。当然,使用这些代码创建的新线程不会做任何有用的事情。线程将启动并几乎立即退出。为了让事情变得更有趣,你需要向posixthreadmain例程函数添加代码来完成一些实际的工作。为了确保线程知道要做什么工作,可以在创建时向它传递一个指向某些数据的指针。将这个指针作为pthread_create函数的最后一个参数传递。

要将新创建的线程中的信息传递回应用程序的主线程,需要在目标线程之间建立通信路径。对于基于c的应用程序,有几种方法可以在线程之间进行通信,包括使用端口、条件或共享内存。对于长期存在的线程,你几乎总是应该设置某种线程间通信机制,以便让应用程序的主线程能够检查线程的状态,或者在应用程序退出时干净地关闭线程。

有关POSIX线程函数的更多信息,请参见pthread手册页。

使用NSObject派生一个线程

在iOS和OS X v10.5及以后版本中,所有对象都有能力生成一个新线程并使用它来执行它们的一个方法。performSelectorInBackground:withObject:方法创建了一个新的分离线程,并使用指定的方法作为新线程的入口点。例如,如果你有一个对象(由变量myObj表示),并且该对象有一个名为doSomething的方法,你想要在后台线程中运行,你可以使用以下代码:

[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];

调用此方法的效果与调用当前对象、选择器和参数对象作为参数的detachNewThreadSelector:toTarget:withObject: 方法的效果相同。使用默认配置立即生成新线程并开始运行。在选择器内部,你必须像配置任何线程一样配置线程。例如,你需要设置一个自动释放池(如果你没有使用垃圾收集),并配置线程的运行循环(如果你计划使用它)。有关如何配置新线程的信息,请参见配置线程属性。

在Cocoa应用程序中使用POSIX线程

虽然NSThread类是在Cocoa应用程序中创建线程的主接口,但是如果这样做更方便的话,你可以自由地使用POSIX线程。例如,如果你已经有了使用POSIX线程的代码,并且不想重写它,那么你可以使用POSIX线程。如果你确实计划在Cocoa应用程序中使用POSIX线程,那么你仍然应该了解Cocoa和线程之间的交互,并遵循以下部分中的指导原则。

保护Cocoa框架

对于多线程应用程序,Cocoa框架使用锁和其他形式的内部同步来确保它们的行为正确。但是,为了防止这些锁在单线程情况下降低性能,在应用程序使用NSThread类生成其第一个新线程之前,Cocoa不会创建它们。如果你仅使用POSIX线程例程生成线程,Cocoa将不会接收到它需要知道你的应用程序现在是多线程的通知。当这种情况发生时,涉及Cocoa框架的操作可能会导致应用程序不稳定或崩溃。

为了让Cocoa知道你打算使用多个线程,你所要做的就是使用NSThread类衍生一个线程并让那个线程立即退出。线程入口点不需要做任何事情。使用NSThread生成一个线程的行为就足以确保Cocoa框架所需的锁被放置到位。

如果你不确定Cocoa是否认为你的应用程序是多线程的,你可以使用NSThread的ismultithread方法来检查。

混合POSIX和Cocoa锁

在同一个应用程序中使用POSIX和Cocoa的混合锁是安全的。Cocoa锁和条件对象本质上只是POSIX互斥体和条件的包装器。但是,对于给定的锁,必须始终使用相同的接口来创建和操作该锁。换句话说,你不能使用Cocoa NSLock对象来操作使用pthread_mutex_init函数创建的互斥锁,反之亦然。

配置线程属性

在创建线程之后(有时在此之前),你可能希望配置线程环境的不同部分。以下部分描述了你可以进行的一些更改,以及你可能在什么时候进行这些更改。

配置线程的堆大小

对于创建的每个新线程,系统在进程空间中分配特定数量的内存,作为该线程的栈。栈管理栈帧,并且也是声明线程的任何局部变量的地方。分配给线程的内存量列在线程代价中。

如果你希望更改给定线程的堆大小,则必须在创建线程之前这样做。所有的线程化技术都提供了某种设置堆栈大小的方法,尽管使用NSThread设置堆栈大小只在iOS和OS X v10.5及更高版本中可用。表2-2列出了每种技术的不同选项。

表2-2设置线程的堆栈大小

技术解释
Cocoa在iOS和OS X v10.5及以后版本中,分配和初始化NSThread对象(不要使用detachNewThreadSelector:toTarget:withObject:method)。在调用thread对象的start方法之前,使用setStackSize:方法来指定新的堆栈大小。
POSIX创建一个新的pthread_attr_t结构,并使用pthread_attr_setstacksize函数更改默认堆栈大小。在创建线程时将属性传递给pthread_create函数。
多处理服务在创建线程时,将适当的堆栈大小值传递给MPCreateTaskfunction。

配置线程本地存储

每个线程维护一个键-值对字典,可以从线程中的任何地方访问它。你可以使用此字典来存储希望在线程执行期间保持的信息。例如,你可以使用它来存储希望在线程的运行循环的多次迭代中保持的状态信息。

Cocoa和POSIX以不同的方式存储线程字典,因此不能混合和匹配对这两种技术的调用。但是,只要在线程代码中坚持使用一种技术,最终的结果应该是类似的。在Cocoa中,使用NSThread对象的threadDictionary方法来检索NSMutableDictionary对象,你可以向该对象添加线程所需的任何键。在POSIX中,使用pthread_setspecific和pthread_getspecific函数设置和获取线程的键和值。

设置线程的分离状态

大多数高级线程技术默认创建分离线程。在大多数情况下,分离线程是首选的,因为它们允许系统在线程完成后立即释放线程的数据结构。分离的线程也不需要与程序进行显式交互。从线程检索结果的方法由你自行决定。相比之下,在另一个线程显式地与该线程连接之前,系统不会回收可连接线程的资源,该进程可能会阻塞执行连接的线程。

你可以将可接合线程看作类似于子线程。尽管它们仍然作为独立的线程运行,但是一个可连接的线程必须由另一个线程连接,然后系统才能回收它的资源。可接合线程还提供了将数据从现有线程传递到另一个线程的显式方式。在可接合线程退出之前,它可以将数据指针或其他返回值传递给pthread_exit函数。然后,另一个线程可以通过调用pthread_join函数来声明这些数据。

重要提示:在应用程序退出时,可以立即终止分离的线程,但不能终止可连接的线程。在允许进程退出之前,必须连接每个可连接的线程。因此,在线程正在执行不应被中断的关键工作(如将数据保存到磁盘)的情况下,可接合线程可能是更好的选择。

如果你确实想要创建可接合线程,那么惟一的方法就是使用POSIX线程。POSIX默认创建可接合的线程。要将线程标记为分离的或可接合的,请在创建线程之前使用pthread_attr_setdetachstate函数修改线程属性。在线程开始之后,可以通过调用pthread_detach函数将可接合线程更改为分离线程。有关这些POSIX线程函数的更多信息,请参见pthread手册页。有关如何与线程联接的信息,请参阅pthread_join手册页。

设置线程优先级

你创建的任何新线程都具有与之关联的默认优先级。内核的调度算法在决定运行哪个线程时考虑线程优先级,优先级高的线程比优先级低的线程更有可能运行。高优先级并不保证线程的特定执行时间,只是与低优先级线程相比,调度器更有可能选择它。

重要提示:保持线程的优先级为默认值通常是一个好主意。增加一些线程的优先级也会增加低优先级线程饿死的可能性。如果你的应用程序包含高优先级和低优先级线程,它们必须相互交互,那么低优先级线程的不足可能会阻塞其他线程并造成性能瓶颈。

如果你确实希望修改线程优先级,Cocoa和POSIX都提供了一种方法。对于Cocoa线程,你可以使用NSThread的setThreadPriority: 类方法来设置当前运行线程的优先级。对于POSIX线程,可以使用pthread_setschedparam函数。有关更多信息,请参见NSThread类引用或pthread_setschedparam手册页。

编写线程输入例程

在大多数情况下,在OS X中线程入口点例程的结构与在其他平台上是相同的。你可以初始化你的数据结构,做一些工作,或者有选择地设置一个运行循环,当你的线程代码完成时清理。根据你的设计,在编写输入例程时可能需要采取一些额外的步骤。

创建自动释放池

在Objective-C框架中链接的应用程序通常必须在每个线程中创建至少一个自动释放池。如果应用程序使用托管模型(应用程序在其中处理对象的保留和释放),那么autorelease池将捕获从该线程自动释放的任何对象。

如果应用程序使用垃圾收集而不是托管内存模型,则没有必要严格地创建自动释放池。在垃圾收集的应用程序中存在一个自动释放池是无害的,并且在大多数情况下都被忽略了。在代码模块必须同时支持垃圾收集和托管内存模型的情况下,它是允许的。在这种情况下,必须提供自动释放池来支持托管内存模型代码,如果应用程序在启用垃圾收集的情况下运行,则自动释放池将被忽略。

如果你的应用程序使用托管内存模型,那么创建一个自动释放池应该是你在线程输入例程中要做的第一件事。类似地,销毁这个自动释放池应该是你在线程中做的最后一件事。这个池确保自动释放对象被捕获,尽管它直到线程本身退出才释放它们。清单2-2显示了使用自动释放池的基本线程条目例程的结构。

清单2-2定义线程入口点例程

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
 
    // Do thread work here.
 
    [pool release];  // Release the objects in the pool.
}

因为顶级的自动释放池在线程退出之前不会释放它的对象,所以长寿线程应该创建更多的自动释放池来更频繁地释放对象。例如,使用运行循环的线程可能在每次运行循环时创建并释放一个自动释放池。更频繁地释放对象可以防止应用程序的内存占用过大,从而导致性能问题。但是,与任何与性能相关的行为一样,你应该度量代码的实际性能,并适当地调整自动释放池的使用。

有关内存管理和自动释放池的更多信息,请参阅高级内存管理编程指南。

设置异常处理程序

如果你的应用程序捕获并处理异常,那么你的线程代码应该准备捕获任何可能发生的异常。虽然最好是在异常可能发生的地方处理异常,但是在线程中捕获抛出的异常失败会导致应用程序退出。在线程入口例程中安装最后一个try/catch,可以捕获任何未知的异常并提供适当的响应。

在Xcode中构建项目时,可以使用c++或Objective-C异常处理风格。有关设置如何在Objective-C中引发和捕获异常的信息,请参阅异常编程主题。

设置运行循环

在编写希望在单独线程上运行的代码时,有两个选项。第一个选项是将线程的代码编写为一个长任务,执行时很少或没有中断,并在线程完成时退出。第二个选项是将线程放入循环中,并让它在请求到达时动态地处理它们。第一个选项不需要对代码进行特殊设置;你只要开始做你想做的工作。但是,第二个选项涉及到设置线程的运行循环。

OS X和iOS为在每个线程中实现运行循环提供了内置支持。应用程序框架会自动启动应用程序主线程的运行循环。如果创建任何辅助线程,则必须配置run循环并手动启动它。

有关使用和配置运行循环的信息,请参阅运行循环。

终止一个线程

退出线程的推荐方法是让它正常退出其入口点例程。虽然Cocoa、POSIX和多处理服务提供了直接杀死线程的例程,但是强烈建议不要使用这种例程。杀死一个线程会阻止该线程在其自身之后进行清理。线程分配的内存可能会被泄漏,线程当前使用的任何其他资源可能无法被正确清理,从而在以后产生潜在的问题。

如果你预期需要在操作过程中终止某个线程,则应该从一开始就设计线程来响应取消或退出消息。对于长时间运行的操作,这可能意味着周期性地停止工作,并检查是否有这样的消息到达。如果确实有消息请求线程退出,那么线程就有机会执行任何必要的清理并优雅地退出;否则,它只需返回工作并处理下一块数据。

响应取消消息的一种方法是使用运行循环输入源来接收此类消息。清单2-3显示了该代码在线程主入口例程中的结构。(示例只显示了主循环部分,不包括设置自动释放池或配置实际工作的步骤。)该示例在运行循环上安装了一个自定义输入源,可以从另一个线程发送消息;有关设置输入源的信息,请参见配置运行循环源。在执行总工作量的一部分之后,线程将简单地运行run循环,以查看消息是否到达输入源。如果没有,run循环将立即退出,循环将继续执行下一个工作块。因为处理程序不能直接访问exitNow局部变量,所以退出条件通过线程字典中的键-值对进行通信。

清单2-3检查长作业期间的退出条件

- (void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
 
    // Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
 
    // Install an input source.
    [self myInstallCustomInputSource];
 
    while (moreWorkToDo && !exitNow)
    {
        // Do one chunk of a larger body of work here.
        // Change the value of the moreWorkToDo Boolean when done.
 
        // Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];
 
        // Check to see if an input source handler changed the exitNow value.
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}

运行循环run loop

Run循环是与线程相关的基本基础结构的一部分。run循环是一个事件处理循环,你可以使用它来调度工作并协调传入事件的接收。run循环的目的是在有工作要做时让线程保持忙碌,在没有工作时让线程休眠。

运行循环管理不是完全自动的。你仍然必须设计线程的代码,以便在适当的时间启动运行循环并响应传入的事件。Cocoa和Core Foundation都提供了运行循环对象来帮助你配置和管理线程的运行循环。你的应用程序不需要显式地创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的run loop对象。但是,只有辅助线程需要显式地运行它们的运行循环。作为应用程序启动过程的一部分,应用程序框架会在主线程上自动设置并运行运行循环。

以下部分提供了关于运行循环以及如何为应用程序配置它们的更多信息。有关运行循环对象的更多信息,请参见NSRunLoop类引用和CFRunLoop引用。

对运行循环的剖析

run循环和它的名字听起来很像。它是线程进入的一个循环,用于运行事件处理程序来响应传入的事件。你的代码提供了用于实现运行循环的实际循环部分的控制语句——换句话说,你 的代码提供了驱动运行循环的while或for循环。在循环中,使用run loop对象“运行”接收事件并调用安装的处理程序的事件处理代码。

运行循环从两种不同类型的源接收事件。输入源传递异步事件,通常是来自另一个线程或另一个应用程序的消息。定时器源交付同步事件,在预定的时间或重复的间隔发生。这两种类型的源都使用特定于应用程序的处理程序例程来处理事件。

图3-1显示了运行循环的概念结构和各种来源。输入源将异步事件传递给相应的处理程序,并触发runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出。计时器源将事件交付给它们的处理程序例程,但不会导致运行循环退出。

图3-1运行循环的结构及其来源

运行循环的结构及其事件源

除了处理输入源之外,run循环还生成关于运行循环行为的通知。注册的运行循环观察者可以接收这些通知,并使用它们对线程进行额外的处理。你可以使用Core Foundation在线程上安装运行循环观察者。

以下部分提供了有关运行循环的组件及其操作模式的更多信息。它们还描述了在处理事件期间的不同时间生成的通知。

运行循环模式

运行循环模式是要监视的输入源和计时器的集合,以及要通知的运行循环观察者的集合。每次运行run循环时,都要(显式或隐式)指定要运行的特定“模式”。在运行循环的传递过程中,只监视与该模式相关联的源,并允许交付它们的事件。(类似地,只有与该模式相关的观察者才会被通知运行循环的进度。)与其他模式相关联的源保留任何新事件,直到后续的事件以适当的模式通过循环。

在你的代码中,你通过名称来标识模式。Cocoa和Core Foundation都定义了默认模式和几种常用模式,以及用于在代码中指定这些模式的字符串。你可以通过简单地为模式名称指定一个自定义字符串来定义自定义模式。尽管你分配给自定义模式的名称是任意的,但是这些模式的内容不是任意的。你必须确保将一个或多个输入源、计时器或运行循环观察者添加到为其创建的任何模式中,以使其有用。

在运行循环的特定遍历期间,使用模式过滤来自不需要的源的事件。大多数情况下,你希望以系统定义的“默认”模式运行运行循环。然而,模态面板可以在“模态”模式下运行。在此模式下,只有与模式面板相关的源才会将事件交付给线程。对于辅助线程,可以使用自定义模式来防止低优先级源在时间关键型操作期间交付事件。

注意:模式区分基于事件的来源,而不是事件的类型。例如,你不会使用模式来仅匹配鼠标向下事件或仅匹配键盘事件。你可以使用模式监听一组不同的端口,临时挂起计时器,或者以其他方式更改源并运行当前被监视的循环观察者。

表3-1列出了Cocoa和Core Foundation定义的标准模式,以及使用该模式的描述。name列列出用于在代码中指定模式的实际常量。

表3-1预定义的运行循环模式

模式名称描述
DefaultNSDefaultRunLoopMode(Cocoa)kCFRunLoopDefaultMode (Core Foundation)默认模式是用于大多数操作的模式。大多数情况下,应该使用此模式启动运行循环并配置输入源。
ConnectionNSConnectionReplyMode(Cocoa)Cocoa将此模式与NSConnection对象结合使用以监视应答。你自己很少需要使用这种模式。
ModalNSModalPanelRunLoopMode(Cocoa)Cocoa使用此模式来识别用于模态面板的事件。
Event trackingNSEventTrackingRunLoopMode(Cocoa)Cocoa使用这种模式来限制鼠标拖动循环和其他用户界面跟踪循环期间的传入事件。
Common modesNSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes (Core Foundation)这是一组可配置的常用模式。将输入源与此模式关联也将其与组中的每个模式关联。对于Cocoa应用程序,默认情况下,这个集合包括默认的、模态的和事件跟踪模式。Core Foundation最初只包含默认模式。你可以使用CFRunLoopAddCommonModefunction将自定义模式添加到集合中。

输入源

输入源将事件异步传递到线程。事件的源取决于输入源的类型,输入源通常分为两类。基于端口的输入源监视应用程序的Mach端口自定义输入源监视自定义事件源。就运行循环而言,输入源是基于端口的还是自定义的应该无关紧要。系统通常实现两种类型的输入源,你可以直接使用它们。这两个源之间唯一的区别是它们是如何被标记的。基于端口的源由内核自动发出信号,而自定义源必须由另一个线程手动发出信号。

创建输入源时,将其分配给运行循环的一个或多个模式。模式影响在任何给定时刻监视哪些输入源。大多数情况下,你以默认模式运行run循环,但是你也可以指定自定义模式。如果输入源没有处于当前监视模式,则它生成的任何事件都将一直保持,直到运行循环以正确的模式运行。

下面几节将介绍一些输入源。

基于端口的源

Cocoa和Core Foundation为使用与端口相关的对象和函数创建基于端口的输入源提供内置支持。例如,在Cocoa中,根本不需要直接创建输入源。你只需创建一个端口对象,并使用NSPort的方法将该端口添加到运行循环中。port对象为你处理所需输入源的创建和配置。

在Core Foundation中,必须手动创建端口及其运行循环源。在这两种情况下,都使用与端口不透明类型(CFMachPortRef、CFMessagePortRef或CFSocketRef)相关联的函数来创建适当的对象。

有关如何设置和配置自定义基于端口的源的示例,请参见配置基于端口的输入源。

自定义输入源

要创建自定义输入源,必须使用Core Foundation中与CFRunLoopSourceRef不透明类型相关联的函数。使用多个回调函数配置自定义输入源。Core Foundation在不同的位置调用这些函数来配置源、处理任何传入事件,并在从运行循环中删除源时销毁源。

除了定义事件到达时自定义源的行为外,还必须定义事件交付机制。源的这一部分运行在一个单独的线程上,负责向输入源提供它的数据,并在数据准备好进行处理时通知它。事件交付机制由你决定,但不必过于复杂。

有关如何创建自定义输入源的示例,请参见定义自定义输入源。有关自定义输入源的参考信息,请参见CFRunLoopSource参考。

Cocoa执行选择器源

除了基于端口的源之外,Cocoa还定义了一个自定义输入源,允许你在任何线程上执行选择器。与基于端口的源类似,执行选择器请求在目标线程上序列化,从而缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同,执行选择器源在执行选择器后将自己从运行循环中移除。

注意:在OS X v10.5之前,执行选择器源主要用于向主线程发送消息,但是在OS X v10.5及其后版本和iOS中,你可以使用它们向任何线程发送消息。

在另一个线程上执行选择器时,目标线程必须有一个活动的运行循环。对于创建的线程,这意味着要等到代码显式地启动运行循环。但是,由于主线程启动了自己的运行循环,所以只要应用程序调用应用程序的代理方法applicationDidFinishLaunching:,run循环每次通过循环处理所有排队的执行选择器调用,而不是在每次循环迭代期间处理一个选择器调用。

表3-2列出了在NSObject上定义的可用于在其他线程上执行选择器的方法。因为这些方法是在NSObject上声明的,所以你可以在任何你可以访问Objective-C对象的线程中使用它们,包括POSIX线程。这些方法实际上并不创建一个新线程来执行选择器。

表3-2在其他线程上执行选择器

方法解释
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在应用程序的主线程的下一个运行循环周期中执行指定的选择器。这些方法为你提供了在执行选择器之前阻塞当前线程的选项。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在任何有NSThread对象的线程上执行指定的选择器。这些方法为你提供了在执行选择器之前阻塞当前线程的选项。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在下一个运行循环周期中和一个可选的延迟期之后,在当前线程上执行指定的选择器。因为它要等到下一个运行循环周期才执行选择器,所以这些方法提供了当前执行代码的一个自动小延迟。多个队列选择器按其排队顺序依次执行。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
允许你取消使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法发送到当前线程的消息。

计时器timer源

定时器源会在将来的某个预设时间将事件同步发送到线程。定时器是线程通知自己做某事的一种方式。例如,搜索字段可以使用计时器在用户连续的击键之间经过一定时间后启动自动搜索。使用这个延迟时间可以让用户在开始搜索之前尽可能多地输入所需的搜索字符串。

尽管定时器生成基于时间的通知,但它并不是一种实时机制。与输入源一样,计时器与运行循环的特定模式相关联。如果计时器没有处于运行循环当前监视的模式中,则在你以计时器支持的模式之一运行运行循环之前,它不会触发。类似地,如果计时器在运行循环正在执行处理程序例程时触发,则计时器将等待下一次通过运行循环调用其处理程序例程。如果run循环根本不运行,计时器就不会触发。

你可以配置计时器来只生成一次或多次事件。重复计时器根据预定的触发时间(而不是实际的触发时间)自动重新调度自己。例如,如果一个计时器计划在一个特定的时间触发,并且之后每5秒触发一次,那么计划的触发时间将始终落在最初的5秒间隔上,即使实际的触发时间被延迟。如果触发时间延迟太久,以至于错过了一个或多个预定的触发时间,则在错过的时间段内只触发计时器一次。在触发错过的时间段之后,计时器被重新安排为下一次预定的触发时间。

有关配置计时器源的更多信息,请参见配置计时器源。有关参考信息,请参阅NSTimer类参考或CFRunLoopTimer参考。

运行循环观察者

与在适当的异步或同步事件发生时触发的源不同,运行循环观察者在运行循环本身的执行期间在特殊位置触发。你可以使用运行循环观察者来准备线程来处理给定的事件,或者在线程进入睡眠状态之前准备线程。你可以将运行循环观察者与运行循环中的以下事件关联起来:

  • 运行循环的入口。
  • 当运行循环即将处理计时器时。
  • 当运行循环即将处理输入源时。
  • 当运行循环即将进入睡眠状态时。
  • 当运行循环已经唤醒,但在它处理唤醒它的事件之前。
  • 退出运行循环。

你可以使用Core Foundation将运行循环观察者添加到应用程序中。要创建一个运行循环观察者,需要创建一个CFRunLoopObserverRef不透明类型的新实例。此类型跟踪自定义回调函数及其感兴趣的活动。

与计时器类似,运行循环观察者可以使用一次或多次。一次性观察者在触发后从运行循环中删除自己,而重复的观察者则保持连接。在创建观察者时,指定它是运行一次还是重复运行。

有关如何创建运行循环观察者的示例,请参见配置运行循环。有关参考信息,请参见CFRunLoopObserver引用。

事件的运行循环序列

每次运行它时,线程的运行循环都会处理挂起的事件并为任何附加的观察者生成通知。它这样做的顺序是非常具体的,如下:

  1. 通知观察者运行循环已经被输入。
  2. 通知观察者,任何准备就绪的计时器即将启动。
  3. 通知观察者,任何不是基于端口的输入源都将触发。
  4. 启动任何准备启动的非基于端口的输入源。
  5. 如果基于端口的输入源已经准备好并等待触发,则立即处理事件。转到第9步。
  6. 通知观察者线程即将休眠。
  7. 让线程休眠,直到发生以下事件之一:
    • 基于端口的输入源的事件到达。
    • 一个计时器启动。
    • 为运行循环设置的超时值过期。
    • run循环被显式地唤醒。
  8. 通知观察者线程刚刚唤醒。
  9. 处理挂起事件。
    • 如果用户定义的计时器触发,则处理计时器事件并重新启动循环。转到步骤2。
    • 如果触发了输入源,则传递事件。
    • 如果运行循环被显式唤醒,但尚未超时,则重新启动循环。转到步骤2。
  10. 通知观察者运行循环已经退出。

由于计时器和输入源的观察者通知是在这些事件实际发生之前交付的,因此通知的时间与实际事件的时间之间可能存在差距。如果这些事件之间的时间间隔非常重要,那么可以使用sleep和从sleep唤醒的通知来帮助你关联实际事件之间的时间间隔。

因为计时器和其他周期性事件是在运行run循环时交付的,所以绕过该循环会破坏这些事件的交付。当你通过输入一个循环并从应用程序中重复请求事件来实现一个鼠标跟踪例程时,就会发生这种行为的典型例子。由于你的代码直接捕获事件,而不是让应用程序正常地调度这些事件,活动计时器将无法触发,直到你的鼠标跟踪例程退出并将控制权返回给应用程序。

可以使用run loop对象显式地唤醒运行循环。其他事件也可能导致run循环被唤醒。例如,添加另一个非基于端口的输入源将唤醒运行循环,以便可以立即处理输入源,而不是等待其他事件发生。

什么时候使用运行循环?

只有在为应用程序创建辅助线程时才需要显式地运行运行循环。应用程序主线程的运行循环是基础设施的关键部分。因此,app框架提供了运行主应用程序循环的代码,并自动启动该循环。iOS中的UIApplication(或OS X中的NSApplication)的run方法作为正常启动序列的一部分启动应用程序的主循环。如果使用Xcode模板项目来创建应用程序,则永远不必显式地调用这些例程。

对于辅助线程,你需要决定是否需要运行循环,如果需要,则自己配置并启动它。在所有情况下都不需要启动线程的运行循环。例如,如果使用线程执行某些长时间运行和预先确定的任务,则可以避免启动run循环。Run循环用于需要与线程进行更多交互的情况。例如,你需要开始一个运行循环,如果你打算做以下任何一种:

  • 使用端口或自定义输入源与其他线程通信。
  • 在线程上使用计时器。
  • 在Cocoa应用程序中使用performSelector…方法。
  • 保留线程以执行周期性任务。

如果选择使用run循环,则配置和设置非常简单。但是,与所有线程编程一样,你应该有一个在适当情况下退出辅助线程的计划。通过让线程退出来干净地结束线程总是比强制终止好。有关如何配置和退出运行循环的信息在使用运行循环对象中进行了描述。

使用运行循环对象

run loop对象提供了主接口,用于向运行循环添加输入源、计时器和运行循环观察者,然后运行它。每个线程都有一个与之关联的run loop对象。在Cocoa中,这个对象是NSRunLoop类的一个实例。在低级应用程序中,它是指向CFRunLoopRef不透明类型的指针。

获取一个运行循环对象

要获得当前线程的运行循环,你可以使用以下方法之一:

  • 在Cocoa应用程序中,使用NSRunLoop的currentRunLoop类方法来检索NSRunLoop对象。
  • 使用CFRunLoopGetCurrent函数。

尽管它们不是免费的桥接类型,但在需要时,你可以从NSRunLoop对象获取CFRunLoopRef不透明类型。NSRunLoop类定义了一个getCFRunLoop方法,它返回一个可以传递给Core Foundation例程的CFRunLoopRef类型。因为这两个对象引用同一个运行循环,所以你可以根据需要混合调用NSRunLoop对象和CFRunLoopRef不透明类型。

配置运行循环

在次要线程上运行运行循环之前,必须向其添加至少一个输入源或计时器。如果运行循环没有任何要监视的源,则在尝试运行它时立即退出。有关如何将源代码添加到运行循环的示例,请参见配置运行循环源代码。

除了安装源代码之外,你还可以安装运行循环观察者,并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察者,你需要创建一个CFRunLoopObserverRef不透明类型,并使用CFRunLoopAddObserver函数将其添加到运行循环中。必须使用Core Foundation创建运行循环观察者,即使对于Cocoa应用程序也是如此。

清单3-1显示了一个线程的主例程,该线程将一个运行循环观察者附加到它的运行循环中。这个示例的目的是向你展示如何创建一个运行循环观察者,因此代码只需设置一个运行循环观察者来监视所有运行循环活动。基本处理程序例程(未显示)只是在处理计时器请求时记录运行循环活动。

清单3-1创建一个运行循环观察者

- (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循环时,最好添加至少一个输入源来接收消息。虽然你可以在仅附加计时器的情况下进入运行循环,但是一旦计时器触发,它通常会失效,这将导致运行循环退出。附加一个重复计时器可以使运行循环在更长的时间内运行,但是需要周期性地触发计时器来唤醒线程,这实际上是轮询的另一种形式。相反,输入源等待事件发生,让线程处于休眠状态。

启动运行循环

只有对应用程序中的辅助线程才需要启动run循环。运行循环必须至少有一个输入源或计时器来监视。如果没有附加,则run循环立即退出。

有几种方式来启动运行循环,包括以下:

  • 无条件地
  • 有固定的时间限制
  • 在特定模式下

无条件地进入运行循环是最简单的选择,但也是最不可取的。无条件地运行运行循环会将线程放入一个永久循环,这使你对运行循环本身的控制非常少。你可以添加和删除输入源和计时器,但是停止运行循环的惟一方法是终止它。也没有办法在自定义模式下运行运行循环。

与其无条件地运行run循环,不如使用超时值运行run循环。当使用超时值时,run循环将一直运行,直到事件到来或分配的时间过期。如果事件到达,则将该事件分派给处理程序进行处理,然后运行循环退出。然后,你的代码可以重新启动run循环来处理下一个事件。如果分配的时间过期,你可以简单地重新启动运行循环,或者使用该时间执行任何需要的日常工作。

除了超时值之外,还可以使用特定的模式运行运行循环。模式和超时值不是互斥的,它们都可以在启动运行循环时使用。模式限制了向运行循环交付事件的源的类型,并且在运行循环模式中有更详细的描述。

清单3-2显示了线程主入口例程的框架版本。本例的关键部分显示了运行循环的基本结构。实际上,你将输入源和计时器添加到运行循环中,然后反复调用其中一个例程来启动运行循环。每次运行循环例程返回时,都要检查是否出现了需要退出线程的条件。该示例使用Core Foundation运行循环例程,以便检查返回结果并确定运行循环退出的原因。如果你使用Cocoa并且不需要检查返回值,你也可以使用NSRunLoop类的方法以类似的方式运行运行循环。(有关调用NSRunLoop类方法的运行循环的示例,请参见清单3-14。)

清单3-2运行一个run循环

- (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.
}

可以递归地运行一个运行循环。换句话说,你可以调用CFRunLoopRun、CFRunLoopRunInMode或任何用于从输入源或计时器的处理程序例程中启动运行循环的NSRunLoop方法。这样做时,你可以使用任何想要运行嵌套运行循环的模式,包括外部运行循环使用的模式。

退出运行循环

有两种方法使运行循环退出之前,它已经处理了一个事件:

  • 将run循环配置为使用超时值运行。
  • 告诉run循环停止。

如果能够管理超时值,则使用超时值当然是更好的选择。指定超时值可以让运行循环在退出之前完成所有的正常处理,包括向运行循环观察者发送通知。

使用CFRunLoopStop函数显式地停止运行循环会产生类似于超时的结果。run循环发送所有剩余的run-loop通知,然后退出。不同之处在于,你可以在无条件启动的run循环中使用此技术。

尽管删除运行循环的输入源和计时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环中以处理所需的事件。因为你的代码可能不知道这些输入源,所以无法删除它们,这将阻止run循环退出。

线程安全和运行循环对象

线程安全取决于使用哪个API来操作运行循环。Core Foundation中的函数通常是线程安全的,可以从任何线程调用。但是,如果你正在执行更改运行循环配置的操作,则最好尽可能从拥有运行循环的线程开始执行。

Cocoa的NSRunLoop类并不像它的核心基础类那样是线程安全的。如果你正在使用NSRunLoop类来修改你的运行循环,那么你应该只从拥有该运行循环的同一个线程进行修改。将输入源或计时器添加到属于不同线程的运行循环中可能会导致代码崩溃或以意想不到的方式运行。

配置运行循环源

下面几节将展示如何在Cocoa和Core Foundation中设置不同类型的输入源。

定义自定义输入源

创建自定义输入源需要定义以下内容:

  • 你希望输入源处理的信息。
  • 一个调度程序例程,让有兴趣的客户知道如何联系你的输入源。
  • 执行任何客户端发送的请求的处理程序例程。
  • 一个使你的输入源无效的取消程序。

因为你创建了一个自定义输入源来处理自定义信息,所以实际的配置是灵活的。调度器、处理程序和取消例程是定制输入源几乎总是需要的关键例程。然而,其余大部分输入源行为发生在这些处理程序例程之外。例如,由你定义将数据传递到输入源以及将输入源的存在与其他线程通信的机制。

图3-2显示了一个定制输入源的配置示例。在本例中,应用程序的主线程维护对输入源、该输入源的自定义命令缓冲区和安装输入源的运行循环的引用。当主线程有一个任务要传递给工作线程时,它会向命令缓冲区发送一个命令以及工作线程启动任务所需的任何信息。(因为主线程和工作线程的输入源都可以访问命令缓冲区,所以必须同步访问。)一旦发出该命令,主线程将向输入源发出信号,并唤醒工作线程的运行循环。在接收到wake up命令后,run循环调用输入源的处理程序,该处理程序处理在命令缓冲区中找到的命令。

图3-2操作自定义输入源

操作自定义输入源

下面几节解释前面图中自定义输入源的实现,并显示需要实现的关键代码。

定义输入源

定义自定义输入源需要使用Core Foundation例程来配置运行循环源并将其附加到运行循环。尽管基本的处理程序是基于C的函数,但这并不妨碍你为这些函数编写包装程序,并使用Objective-C或c++实现代码主体。

图3-2中引入的输入源使用Objective-C对象来管理命令缓冲区并与运行循环进行协调。清单3-3显示了这个对象的定义。RunLoopSource对象管理一个命令缓冲区,并使用该缓冲区接收来自其他线程的消息。此清单还显示了RunLoopContext对象的定义,它实际上只是一个容器对象,用于将RunLoopSource对象和run循环引用传递给应用程序的主线程。

清单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代码管理输入源的自定义数据,但将输入源附加到运行循环需要基于c的回调函数。当你实际将运行循环源附加到运行循环中时,将调用第一个函数,如清单3-4所示。因为这个输入源只有一个客户机(主线程),所以它使用调度器函数来发送消息,将自己注册到该线程上的应用程序委托。当委托希望与输入源进行通信时,它使用RunLoopContext对象中的信息来进行通信。

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方法,然后该方法处理命令缓冲区中出现的任何命令。

清单3-5在输入源中执行工作

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}

如果你曾经使用CFRunLoopSourceInvalidate函数将输入源从其运行循环中删除,则系统将调用输入源的取消例程。你可以使用这个例程通知客户端你的输入源不再有效,他们应该删除对它的任何引用。清单3-6显示了用RunLoopSource对象注册的取消回调例程。这个函数向应用程序委托发送另一个RunLoopContext对象,但是这一次要求委托删除对运行循环源的引用。

清单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-7显示了RunLoopSource类的init和addToCurrentRunLoop方法。init方法创建了实际必须附加到运行循环的CFRunLoopSourceRef不透明类型。它将RunLoopSource对象本身作为上下文信息传递,以便回调例程拥有指向该对象的指针。直到工作线程调用addToCurrentRunLoop方法,此时会调用runloopsourcesch调度eroutine回调函数,才会安装输入源。一旦输入源被添加到运行循环中,线程就可以运行它的运行循环来等待它。

清单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);
}

与输入源的客户进行协调

要使输入源有用,需要操作它并从另一个线程发出信号。输入源的关键是将其关联的线程置为睡眠状态,直到有事可做为止。这个事实需要让应用程序中的其他线程知道输入源,并有方法与它通信。

通知客户端关于输入源的一种方法是在输入源首次安装到其运行循环中时发送注册请求。你可以向任意数量的客户注册输入源,或者你可以简单地向某个中央代理注册输入源,然后将输入源出售给感兴趣的客户。清单3-8显示了应用程序委托定义的注册方法,并在调用RunLoopSource对象的调度程序函数时调用。此方法接收由RunLoopSource对象提供的RunLoopContext对象,并将其添加到其源列表中。此清单还显示了从运行循环中删除输入源时用于取消注册的例程。

清单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所示。

给输入源发信号

在将数据传递给输入源之后,客户机必须向源发出信号并唤醒它的运行循环。向源发出信号让运行循环知道源已准备好进行处理。由于信号出现时线程可能处于休眠状态,所以应该始终显式地唤醒运行循环。如果不这样做,可能会导致处理输入源的延迟。

清单3-9显示了RunLoopSource对象的fireCommandsOnRunLoop方法。当客户机准备好让源处理它们添加到缓冲区中的命令时,它们将调用此方法。

清单3-9唤醒运行循环

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

注意:永远不要试图通过消息传递自定义输入源来处理SIGHUP或其他类型的进程级信号。唤醒运行循环的Core Foundation函数不是信号安全的,不应该在应用程序的信号处理程序例程中使用。有关信号处理程序例程的更多信息,请参见sigaction手册页。

配置定时器的源

要创建一个计时器源,你所要做的就是创建一个计时器对象并将其调度到运行循环中。在Cocoa中,你使用NSTimer类来创建新的计时器对象,在Core Foundation中,你使用CFRunLoopTimerRef不透明类型。在内部,NSTimer类只是Core Foundation的一个扩展,它提供了一些方便的特性,比如能够使用相同的方法创建和调度计时器。

在Cocoa中,你可以使用下面的类方法来创建和调度一个计时器:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • scheduledTimerWithTimeInterval:invocation:repeats:

这些方法创建计时器并将其以默认模式(NSDefaultRunLoopMode)添加到当前线程的运行循环中。如果需要,还可以手动调度计时器,方法是创建NSTimer对象,然后使用NSRunLoop的addTimer:forMode:方法将其添加到运行循环中。这两种技术做的基本上是一样的事情,但给你不同程度的控制定时器的配置。例如,如果你创建计时器并手动将其添加到运行循环中,则可以使用默认模式以外的模式来实现。清单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函数配置计时器所需的代码。虽然这个示例没有在上下文结构中传递任何用户定义的信息,但是你可以使用这个结构来传递计时器所需的任何自定义数据。有关此结构内容的更多信息,请参见CFRunLoopTimer参考资料中的描述。

清单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对象建立本地连接,需要创建port对象并将其添加到主线程的运行循环中。启动辅助线程时,将相同的对象传递给线程的入口点函数。辅助线程可以使用相同的对象将消息发送回主线程。

实现主线程代码

清单3-12显示了启动辅助工作线程的主线程代码。因为Cocoa框架执行了许多配置端口和运行循环的中间步骤,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:方法。当数据到达线程自己的本地端口时调用此方法。当一个check-in消息到达时,该方法直接从端口消息检索辅助线程的端口,并保存它供以后使用。

清单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显示了设置工作线程的代码。在为线程创建了一个autorelease池之后,该方法创建一个worker对象来驱动线程执行。工作对象的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将一个核心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显示了工作线程的入口点函数。该函数提取主线程的端口名,并使用它创建回主线程的远程连接。然后,该函数为自己创建一个本地端口,在线程的运行循环中安装该端口,并向主线程发送包含本地端口名称的签入消息。

清单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();
}

一旦它进入运行循环,所有发送到线程端口的未来事件都由ProcessClientRequest函数处理。该函数的实现取决于线程执行的工作类型,这里没有显示。

线程同步(待续)

线程安全摘要(待续)

本文主要是翻译Apple的官方文档,本来想全部关于线程的,但是太长了,关于线程同步和线程安全的内容,留待下次有时间再弄。

首先使用线程,最基本的操作就是对线程的生命周期进行管理,另外就是线程间的通信,以及线程同步和线程安全。对于线程安全和线程同步,苹果官方推荐使用的是GCD和操作对象,其实并不需要使用锁,而是使用一个队列,让队列一次执行一个任务达到同步的效果。

而线程通信是开发中最常遇到的问题,比如子线程更新UI,需要借助线程通信才能对UI进行更新,不单是iOS开发中是这样,基本上所有GUI编程都是使用这个模式。

一般来说,我们可以直接使用GCD,但是有一种情况例外,也就是创建一个常驻线程,这就需要使用到run loop了,自定义run loop自然最好使用上面介绍的关于线程通信的内容:自定义普通源(事件源)其实就是线程通信。

赞(0)
未经允许不得转载:srcmini » Objective-C线程技术开发详解

评论 抢沙发

评论前必须登录!