文档地址
介绍
线程是几个能使单个程序同时并发执行多个代码路径成为可能的技术之一。即使像operation objects和Grand Central Dispatch (GCD) 这些新技术为实现同步并发提供了更加现代化和高效的基础框架,OS X 和 iOS依然提供接口来创建和管理线程。
关于线程编程
多年来,极限计算机的性能在计算机核心上主要受制于单个微型处理器的速度。随着单核处理器达到了实际的限制,芯片制造商进而转化为多核设计,使计算机有机会同时执行多个任务。尽管 OS X 使用这些核心执行系统相关的任务,我们自己的程序依然可以通过线程使用它们。
什么是线程
线程是程序内部实现多条执行路径的一种相对轻量级的方式。在系统层面,程序并行运行,系统根据程序的需要分配给每个程序执行时间。然而,在每个程序内部,存在一个或多个执行线程,用于同时或者几乎同时的方式执行不同任务。系统实际管理这些执行线程,安排它们运行在空闲的核心上,并根据需要抢先中断它们去允许其他线程运行。
从技术角度来看,线程是一个内核级和应用级数据结构需要去管理执行代码的组合。内核级结构协调派发事件到线程和优先安排线程到一个有用的核心上。应用级结构包括存储函数调用的调用栈以及应用程序所需管理和操作的线程数据与状态的结构。
在非并发的应用程序中,它们只有一个执行线程。线程以应用程序的主例程以及逐个到实现应用程序整体行为的不同方法和函数的分支开始和结束。相比之下,一个支持多线程的应用程序以一个线程开始,然后根据需要添加更多的执行路径。每个新路径有自己的自定义开始例程运行独立于应用程序主例程的代码。多线程应用程序提供两个非常重要的潜在优势:
- 多线程可以提高应用程序的感知响应能力。
- 多线程可以提高应用程序在多核系统上的实时性能。
如果你的应用程序只有一个线程,那么这个线程要干所有的事。它需要响应事件,更新应用程序窗口,并执行实现应用程序行为的所有计算。问题是,一个线程只能同时干一件事情。因此当一个计算需要执行很长一段时间才能完成的时候会发生什么呢?当你的代码正在忙碌的计算它所需要的值,你的应用程序就停止了响应用户事件和更新窗口。如果这种行为经行了很长的时间,用户可能认为你的应用程序挂起了并且会强制退出。如果把自定义的计算移动到一个分开的线程中,则应用程序的主线程就有空闲及时的响应用户交互。
随着最近多核计算机的普及,线程提供了一种提高某些类型应用程序性能的方法。执行不同任务的线程可以在不同的处理器内核上同时执行,从而使应用程序可以在给定的时间内增加它的工作量。
当然,线程并不是解决应用程序性能问题的灵丹妙药。随着线程提供的好处带来了潜在的问题。在应用程序中具有多个执行路径会给代码增加相当大的复杂性。每个线程都必须与其他线程协调其操作,以防止它破坏应用程序的状态信息。由于单个应用程序中的线程共享相同的内存空间,因此它们可以访问所有相同的数据结构。如果两个线程试图同时操作相同的数据结构,一个线程可能覆盖其他线程的更改这意味着破坏了结果数据结构。即使有适当的保护措施,您仍然需要注意编译器优化,这些优化会在代码中引入细微(而不是那么微妙)的错误。
线程术语
在深入讨论线程及其支持技术之前,有必要定义一些基本术语。
如果您熟悉UNIX系统,则可能会发现本文档对“任务”一词的使用方式不同。在UNIX系统上,术语“任务”有时用于指代正在运行的进程。
本文件采用以下术语:
- 术语线程用于指代代码的单独执行路径。
- 术语进程用于指代正在运行的可执行文件,其可以包含多个线程。
- 术语任务用于指代需要执行的抽象工作概念。
线程的代替品
自己创建线程的一个问题是它们会给代码增加不确定性。线程是一种支持应用程序并发性的相对低级且复杂的方法。如果您不完全了解设计选择的含义,则可能很容易遇到同步或计时问题,其严重性可能从细微的行为更改到应用程序崩溃以及用户数据损坏。
另一个需要考虑的因素是你是否需要线程或并发。线程解决了如何在同一进程内同时执行多个代码路径的特定问题。但是,在某些情况下,您所做的工作量并不能保证并发性。线程在内存消耗和CPU时间方面为您的进程带来了巨大的开销。您可能会发现此开销对于预期任务来说太大了,或者其他选项更容易实现。
表1-1列出了一些线程的替代方案。该表包括线程的替换技术(例如Operation objects和GCD)以及旨在有效使用您已有的单线程的替代方法。
表1-1 线程的替代技术
技术 | 描述 |
---|---|
Operation objects | 在OS X v10.5中引入的operation object是通常在辅助线程上执行的任务的包装器。这个包装器隐藏了执行任务的线程管理方面,让您可以专注于任务本身。您通常将这些对象与操作队列对象结合使用,该操作队列对象实际上管理一个或多个线程上的操作对象的执行。有关如何使用操作对象的更多信息,请参阅“并发编程指南”。 |
Grand Central Dispatch (GCD) | 在Mac OS x v10.6中引入,Grand Central Dispatch是线程的另一种替代方案,可让您专注于执行所需的任务,而不是线程管理。使用GCD,您可以定义要执行的任务并将其添加到工作队列,该队列在适当的线程上处理任务的计划。工作队列考虑到可用内核的数量以及当前负载去执行你的任务比起自己使用线程更高效。有关如何使用GCD和工作队列的信息,请参阅“并发编程指南” |
空闲时间通知 | 对于相对较短且优先级较低的任务,空闲时间通知允许您在应用程序不忙时执行任务。Cocoa使用该NSNotificationQueue对象提供对空闲时间通知的支持。要请求一个空闲时间通知,使用该NSPostWhenIdle选项发送通知到默认的NSNotificationQueue对象。队列延迟通知对象的传递,直到运行循环变为空闲。有关更多信息,请参阅通知编程主题。 |
异步函数 | 这些API可能使用系统守护程序和进程或创建自定义线程来执行其任务并将结果返回给您。(实际的实现是无关紧要的,因为它与代码分离。)在设计应用程序时,查找提供异步行为的函数,并考虑使用它们而不是在自定义线程上使用等效的同步函数。 |
计时器 | 可以在应用程序的主线程上使用计时器来执行定期任务,这些任务太简单而不需要线程,但仍需要定期维护。有关定时器的信息,请参阅定时器源。 |
单独的进程 | 尽管比线程更加重量级,但在任务仅与您的应用程序相关的情况下,创建单独的进程可能很有用。如果任务需要大量内存或必须使用root权限执行,则可以使用进程。例如,您可以使用64位服务器进程计算大型数据集,而32位应用程序将结果显示给用户。 |
注意:当使用fork函数启动单独的进程时,你必须总是用一个exec或者一个相似的函数调用跟随fork的调用。依赖于Core Foundation,Cocoa或Core Data框架(显式或隐式)的应用程序必须对exec函数进行后续调用,否则这些框架可能表现不正常。
线程支持
如果您有现有使用线程的代码,OS X和iOS提供了几种在应用程序中创建线程的技术。此外,两个系统还为管理和同步需要在这些线程上完成的工作提供支持。以下部分描述了在OS X和iOS中使用线程时需要注意的一些关键技术。
线程包
虽然线程的底层实现机制是Mach线程,但很少(如果有的话)使用Mach级别的线程。相反,您通常使用更方便的POSIX API或其衍生产品之一。然而,Mach实现确实提供了所有线程的基本功能,包括抢先执行模型和调度线程的能力,因此它们彼此独立。
清单2-2列出了可以在应用程序中使用的线程技术。
表1-2 线程技术
技术 | 描述 |
---|---|
Cocoa线程 | Cocoa使用NSThread类实现线程。Cocoa还提供NSObject了生成新线程和在已经运行的线程上执行代码的方法。有关更多信息,请参阅使用NSThread和使用NSObject生成线程。 |
POSIX线程 | POSIX线程提供了一个用于创建线程的基于C的接口。如果您没有编写Cocoa应用程序,这是创建线程的最佳选择。POSIX接口使用起来相对简单,并为配置线程提供了充分的灵活性。有关更多信息,请参阅使用POSIX线程 |
多处理服务 | 多处理服务是从旧版Mac OS转换的应用程序使用的基于C的传统接口。此技术仅适用于OS X,任何新开发都应避免使用。相反,您应该使用NSThread类或POSIX线程。如果需要有关此技术的更多信息,请参阅“ 多处理服务编程指南”。 |
在应用程序级别,所有线程的行为方式与其他平台上的行为基本相同。启动线程后,线程以三种主要状态之一运行:running,ready或blocked。如果一个线程当前没有运行,它将被阻塞并等待输入,或者它已准备好运行但尚未安排执行此操作。线程继续在这些状态之间来回移动,直到它最终退出并移动到终止状态。
创建新线程时,必须为该线程指定入口点函数(或Cocoa线程的入口点方法)。此入口点函数构成您要在线程上运行的代码。当函数返回时,或者显式终止线程时,线程将永久停止并由系统回收。由于线程在内存和时间方面的创建成本相对较高,因此建议您的入口点函数执行大量工作或设置运行循环以允许执行重复工作。
有关可用线程技术及其使用方法的更多信息,请参阅线程管理。
Run Loops
运行循环是一个基础结构,用于管理在线程上异步到达的事件。运行循环通过监视线程的一个或多个事件源来工作。当事件到达时,系统唤醒线程并将事件调度到运行循环,然后运行循环将它们分派给您指定的处理程序。如果没有事件存在和准备需处理,则运行循环使线程进入休眠状态。
您不需要对您创建的任何线程使用运行循环,但这样做可以为用户提供更好的体验。运行循环可以创建使用最少量资源的长期线程。因为运行循环在没有任何操作时将其线程置于休眠状态,所以它消除了轮询的需要,这会浪费CPU周期并阻止处理器本身休眠并节省电力。
要配置运行循环,您所要做的就是启动线程,获取对运行循环对象的引用,安装事件处理程序,并告诉运行循环运行。OS X提供的基础结构会自动为您处理主线程的运行循环的配置。但是,如果您计划创建长期存在的辅助线程,则必须自己为这些线程配置运行循环。
中提供了有关运行循环以及如何使用它们的例子详细运行循环。
同步工具
线程编程的一个危险是多线程之间的资源争用。如果多个线程尝试同时使用或修改同一资源,则可能会出现问题。缓解该问题的一种方法是完全消除共享资源,并确保每个线程都有自己独特的资源集来操作。但是,维护完全独立的资源不是一种好的选择,您可能使用锁,条件,原子操作和其他技术来同步对资源的访问。
锁为代码提供强制形式的保护,一次只能由一个线程执行。最常见的锁定类型是互斥锁,也称为互斥锁。当一个线程试图获取当前由另一个线程持有的互斥锁时,它会阻塞,直到另一个线程释放该锁。多个系统框架为互斥锁提供支持,尽管它们都基于相同的底层技术。此外,Cocoa提供了互斥锁的几种变体,以支持不同类型的行为,例如递归。有关可用锁类型的更多信息,请参阅锁。
除锁定外,系统还提供对条件的支持,以确保应用程序中任务的正确排序。条件充当守门人,阻止给定线程,直到它表示的条件变为真。当发生这种情况时,条件会释放线程并允许它继续。POSIX层和Foundation框架都为条件提供直接支持。(如果使用操作对象,则可以配置操作对象之间的依赖关系以对任务的执行进行排序,这与条件提供的行为非常相似。)
虽然锁和条件在并发设计中非常常见,但原子操作是保护和同步数据访问的另一种方法。在可以对标量数据类型执行数学或逻辑运算的情况下,原子操作为锁定提供了轻量级替代。原子操作使用特殊的硬件指令来确保在其他线程有机会访问变量之前完成对变量的修改。
有关可用同步工具的详细信息,请参阅同步工具。
线程间通信
虽然良好的设计可以最大限度地减少所需的通信量,但在某些时候,线程之间的通信变得必要。(线程的工作是为您的应用程序工作,但如果从未使用过该作业的结果,它有什么用处?)线程可能需要处理新的作业请求或将其进度报告给应用程序的主线程。在这些情况下,您需要一种方法来从一个线程获取信息到另一个线程。幸运的是,线程共享相同的进程空间这一事实意味着您有很多通信选项。
线程之间有许多通信方式,每种方式都有自己的优点和缺点。配置线程局部存储列出了可以在OS X中使用的最常见的通信机制。(除了消息队列和Cocoa分布式对象,这些技术在iOS中也可用。)此表中的技术按复杂度递增。
表1-3 通信机制
header 1 | header 2 |
---|---|
直接消息传递 | Cocoa应用程序支持直接在其他线程上执行选择器的能力。此功能意味着一个线程基本上可以在任何其他线程上执行方法。因为它们是在目标线程的上下文中执行的,所以以这种方式发送的消息会在该线程上自动序列化。有关输入源的信息,请参阅Cocoa执行选择器源。 |
全局变量,共享内存和对象 | 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。虽然共享变量快速而简单,但它们也比直接消息传递更脆弱。必须使用锁或其他同步机制小心保护共享变量,以确保代码的正确性。如果不这样做可能会导致竞争条件,数据损坏或崩溃。 |
条件 | 条件是一种同步工具,可用于控制线程何时执行特定代码部分。您可以将条件视为门卫,让线程仅在满足所述条件时运行。有关如何使用条件的信息,请参阅使用条件。 |
运行循环源 | 自定义运行循环源是您设置为在线程上接收特定于应用程序的消息的源。因为它们是事件驱动的,所以当没有任何事情要做时,运行循环源会让你的线程自动进入休眠状态,从而提高线程的效率。有关运行循环和运行循环源的信息,请参阅运行循环。 |
端口和套接字 | 基于端口的通信是两种线程之间通信的更精细的方式,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,端口是使用运行循环源实现的,因此当端口上没有数据等待时,线程会休眠。有关运行循环和基于端口的输入源的信息,请参阅运行循环。 |
消息队列 | 传统的多处理服务定义了用于管理传入和传出数据的先进先出(FIFO)队列抽象。尽管消息队列简单方便,但它们并不像其他一些通信技术那样高效。有关如何使用消息队列的详细信息,请参阅“多处理服务编程指南”。 |
Cocoa分布式对象 | 分布式对象是一种Cocoa技术,可提供基于端口的通信的高级实现。尽管可以将此技术用于线程间通信,但由于其产生的开销量很大,因此非常不鼓励这样做。分布式对象更适合与其他进程通信,其中进程之间的开销已经很高。有关更多信息,请参阅分布式对象编程主题。 |
设计技巧
以下部分提供了一些指导原则,可帮助您以确保代码正确性的方式实现线程。其实一些指南还为自己的线程代码提供实现更好性能的技巧。与任何性能技巧一样,您应始终在更改代码之前,期间和之后收集相关的性能统计信息。
避免明确的创建线程
编写线程创建代码很繁琐且可能容易出错所以要尽量避免。OS X和iOS通过其他API提供对并发的隐式支持。比起创建自己的线程,建议应用异步APIs,GCD或者operation objects来工作。这些技术为您做幕后的线程相关工作,并保证正确执行。此外,通过根据当前系统负载调整活动线程数,GCD和operation objects等技术可以比您自己的代码更有效地管理线程。有关GCD和operation objects的更多信息,请参阅“并发编程指南”。
保持线程合理的忙碌
如果您决定手动创建和管理线程,请记住线程占用宝贵的系统资源。您应该尽力确保分配给线程的任何任务都是合理的活跃和高效的。与此同时,您不应该害怕终止花费大部分时间闲置的线程。线程使用大量内存,其中一些是有线的,因此释放空闲线程不仅有助于减少应用程序的内存占用,还可以释放更多物理内存供其他系统进程使用。
要点: 在开始终止空闲线程之前,应始终记录应用程序当前性能的一组基线度量。尝试更改后,请进行其他测量以验证更改是否实际上提高了性能,而不是损坏它。
避免共享数据结构
避免与线程相关的资源冲突的最简便和最简单的方法是为程序中的每个线程提供它所需的任何数据的副本。当您最小化线程之间的通信和资源争用时,并行代码最有效。
创建多线程应用程序很难。即使您非常小心并在代码中的所有正确接合点处锁定共享数据结构,您的代码仍可能在语义上不安全。例如,如果希望以特定顺序修改共享数据结构,则代码可能会遇到问题。将代码更改为基于事务的模型以进行补偿可能随后会抵消具有多个线程的性能优势。首先消除资源争用通常会导致设计更简单,性能更佳。
线程与用户界面
如果您的应用程序具有图形用户界面,建议您从应用程序的主线程接收与用户相关的事件并启动界面更新。此方法有助于避免与处理用户事件和绘制窗口内容相关的同步问题。某些框架(如Cocoa)通常需要此行为,但即使对于那些不这样做的框架,将此行为保留在主线程上也具有简化管理用户界面的逻辑的优势。
有一些例外值得注意,从其他线程执行图形操作是有利的。例如,您可以使用辅助线程来创建和处理图像,并执行其他与图像相关的计算。使用辅助线程进行这些操作可以大大提高性能。如果您不确定特定的图形操作,请计划从主线程执行此操作
有关使用Cocoa线程安全的更多信息,请参阅线程安全摘要。有关使用Cocoa绘图的更多信息,请参阅“ Cocoa绘图指南”。
退出时留意线程的行为
进程将一直运行,直到所有未分离的线程都退出。默认情况下,只将应用程序的主线程创建为非分离,但您也可以创建其他线程。当用户退出应用程序时,通常认为立即终止所有分离的线程是合适的行为,因为分离线程完成的工作被认为是可选的。但是,如果您的应用程序使用后台线程将数据保存到磁盘或执行其他关键工作,您可能希望将这些线程创建为非分离,以防止在应用程序退出时丢失数据。
将线程创建为非分离(也称为可连接)需要您做额外的工作。由于大多数高级线程技术默认情况下不创建可连接线程,因此您可能必须使用POSIX API来创建线程。此外,您必须在应用程序的主线程中添加代码,以便在最终退出时与非分离线程连接。有关创建可连接线程的信息,请参阅设置线程的分离状态。
如果您正在编写可可应用程序,您还可以使用applicationShouldTerminate:委托方法将应用程序的终止延迟到以后的时间或完全取消它。延迟终止时,您的应用程序需要等待任何关键线程完成其任务,然后调用该replyToApplicationShouldTerminate:方法。有关这些方法的详细信息,请参阅NSApplication类参考。
处理特殊情况
异常处理机制依赖于当前调用堆栈在抛出异常时执行任何必要的清理。因为每个线程都有自己的调用堆栈,所以每个线程都负责捕获自己的异常。未能在辅助线程中捕获异常与未在主线程中捕获异常相同:进程会终止。不能将未捕获的异常抛出到其他线程进行处理。
如果在当前线程捕获到异常需要通知其他线程(如主线程)。你应该捕获异常并简单地向另一个线程发送一条消息,指出发生了什么。根据您的模型和您要执行的操作,捕获异常的线程可以继续处理(如果可能),等待指令,或者只是退出。
注意: 在Cocoa中,NSException对象是一个自包含的对象,能在被捕获时从一个线程传递给另一个线程。
在某些情况下,可能会自动为您创建异常处理程序。例如,@synchronizedObjective-C中的指令包含一个隐式异常处理程序。
干净的终止线程
线程退出的最佳方法当然是让它到达主入口点例程的末尾。虽然有立即终止线程的函数,但这些函数应该仅作为最后的手段使用。在线程到达其自然终点之前终止线程阻碍了线程自行清理。如果线程已分配内存,打开文件或获取其他类型的资源,则代码可能无法回收这些资源,从而导致内存泄漏或其他潜在问题。
有关退出线程的正确方法的更多信息,参阅请终止线程。
代码库的线程安全
虽然应用程序开发人员可以控制应用程序是否使用多个线程执行,但库开发人员却没有。在开发库时,您必须假设调用应用程序是多线程的,或者可以随时切换为多线程。因此,您应始终对代码的关键部分使用锁。
对于库开发人员,仅在应用程序变为多线程时才创建锁是不明智的。如果您需要在某个时刻锁定代码,请在使用库的早期创建锁定对象,最好是在某种初始化库时显式调用。虽然您也可以使用静态库初始化函数来创建此类锁,但只有在没有其他方法时才尝试这样做。执行初始化函数会增加加载库所需的时间,并可能对性能产生负面影响。
注意:始终记得在库代码中平衡lock和unlock互斥锁的调用。还应该记住锁定库中数据结构,而不是依赖调用代码来提供线程安全的环境。
如果您正在开发Cocoa库,则可以为NSWillBecomeMultiThreadedNotification注册观察者,以便在应用程序变为多线程时通知您。但是,您不应该依赖于接收此通知,因为它可能会在调用库代码之前调度线程。