Guide - Threading Programming

162 阅读50分钟

简介

线程是几种技术中的一种,它使得在一个应用程序中并发地执行多个代码路径成为可能。尽管诸如操作对象和Grand Central Dispatch(GCD)等较新的技术为实现并发提供了更现代、更高效的基础设施,但OS X和iOS也提供了创建和管理线程的接口。

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

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

本文件的组织

本文档有以下几章和附录。

关于线程编程介绍了线程的概念以及它们在应用程序设计中的作用。

线程管理提供了关于OS X中的线程技术以及如何使用它们的信息。

运行循环提供了关于如何在二级线程中管理事件处理循环的信息。

同步描述了同步问题和你用来防止多线程破坏数据或使程序崩溃的工具。

线程安全总结提供了OS X和iOS的固有线程安全及其一些关键框架的高级总结。

另见

关于线程的替代品的信息,请参见《并发性编程指南》。

本文仅对POSIX线程API的使用做了简单介绍。关于可用的POSIX线程例程的更多信息,见pthread man页。关于POSIX线程及其使用的更深入的解释,请参阅David R. Butenhof的《用POSIX线程编程》。

关于线程编程

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

什么是线程?

线程是一种相对轻量级的方式,用于在一个应用程序中实现多个执行路径。在系统层面,程序并排运行,系统根据每个程序的需要和其他程序的需要分配执行时间。然而,在每个程序内部,存在一个或多个执行线程,它们可以被用来同时或几乎同时执行不同的任务。系统本身实际上管理着这些执行线程,将它们安排在可用的内核上运行,并根据需要预先中断它们,以便让其他线程运行。

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

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

  1. 多线程可以提高应用程序的感知响应速度。
  2. 多线程可以提高一个应用程序在多核系统上的实时性能

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

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

当然,线程并不是解决应用程序性能问题的万能药。伴随着线程带来的好处,也有潜在的问题。在一个应用程序中拥有多条执行路径会给你的代码增加相当大的复杂性。每个线程都必须与其他线程协调其行动,以防止它破坏应用程序的状态信息。因为单个应用程序中的线程共享相同的内存空间,它们可以访问所有相同的数据结构。如果两个线程试图同时操作相同的数据结构,一个线程可能会覆盖另一个线程的变化,从而破坏所产生的数据结构。即使有适当的保护措施,你仍然要注意编译器的优化,这些优化在你的代码中引入了微妙的(或不那么微妙的)错误。

线程术语

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

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

本文档采用了以下术语。

术语 "线程 "被用来指代代码的独立执行路径。

术语 "进程 "用于指一个正在运行的可执行程序,它可以包含多个线程。

术语 "任务 "用于指需要执行的工作的抽象概念。

线程的可选其他

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

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

表1-1列出了线程的一些替代方案。该表既包括线程的替代技术(如操作对象和GCD),也包括旨在有效利用你已有的单个线程的替代技术。

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

警告:当使用fork函数启动独立的进程时,你必须总是在调用fork之后调用exec或类似的函数。依赖于Core Foundation、Cocoa或Core Data框架的应用程序(无论是明确的还是隐含的)必须随后调用exec函数,否则这些框架可能会表现得不正常。

线程支持

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

线程包

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

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

表1-2 线程技术

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

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

当你创建一个新的线程时,你必须为该线程指定一个入口点函数(如果是Cocoa线程,则是入口点方法)。这个入口点函数构成了你想在线程上运行的代码。当该函数返回时,或者当你明确地终止该线程时,该线程将永久停止并被系统回收。由于创建线程在内存和时间上相对昂贵,因此建议你的入口点函数做大量的工作,或者设置一个运行循环,以允许重复性工作的进行。

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

runloop

运行循环是一个基础设施,用于管理异步到达线程的事件。运行循环通过监控线程的一个或多个事件源来工作。当事件到来时,系统会唤醒线程并将事件分配给运行循环,然后将其分配给你指定的处理程序。如果没有事件出现并准备好被处理,运行循环会让线程进入睡眠状态。

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

要配置一个运行循环,你所要做的就是启动你的线程,获得一个对运行循环对象的引用,安装你的事件处理程序,并告诉运行循环要运行。OS X提供的基础设施会自动为你处理主线程的运行循环的配置问题。然而,如果你打算创建长期存在的次要线程,你必须自己为这些线程配置运行循环。

关于运行循环的细节和如何使用它们的例子在运行循环中提供。

同步化工具

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

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

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

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

关于可用的同步工具的更多信息,见同步工具

线程间的通信

尽管一个好的设计可以最大限度地减少所需的通信量,但在某些时候,线程之间的通信是必要的。(一个线程的工作是为你的应用程序做工作,但如果工作的结果从未被使用,那又有什么用呢?) 线程可能需要处理新的工作请求或向你的应用程序的主线程报告它们的进展。在这些情况下,你需要一种方法来获得从一个线程到另一个线程的信息。幸运的是,线程共享同一进程空间的事实意味着你有很多通信的选择。

有许多方法可以在线程之间进行通信,每一种方法都有自己的优势和劣势。配置线程本地存储列出了你在OS X中可以使用的最常见的通信机制。(除了消息队列和Cocoa分布式对象之外,这些技术在iOS中也是可用的)。本表中的技术是按照复杂程度的增加而列出的。

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

设计提示

以下章节提供了一些指导原则,以帮助你以确保代码正确性的方式实现线程。其中一些准则还提供了一些提示,以使你自己的线程代码获得更好的性能。与任何性能提示一样,在对代码进行修改之前、期间和之后,你都应该收集相关的性能统计数据。

避免明确地创建线程

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

让你的线程保持合理的忙碌

如果你决定手动创建和管理线程,请记住,线程会消耗宝贵的系统资源。你应该尽力确保你分配给线程的任何任务都是合理的长寿和富有成效的。同时,你不应该害怕终止那些大部分时间都在闲置的线程。线程使用了大量的内存,其中一些是有线的,所以释放一个空闲的线程不仅有助于减少你的应用程序的内存占用,它也释放了更多的物理内存给其他系统进程使用。

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

避免共享数据结构

避免与线程有关的资源冲突的最简单和最容易的方法是给你的程序中的每个线程提供它所需要的任何数据的自己的副本。当你把线程之间的通信和资源争夺降到最低时,并行代码的效果最好。

创建一个多线程的应用程序是很难的。即使你非常小心,在代码中所有正确的节点上锁定共享数据结构,你的代码仍然可能在语义上不安全。例如,如果你的代码希望共享数据结构以特定的顺序被修改,就会遇到问题。把你的代码改成基于事务的模型来补偿,随后可能会否定拥有多线程的性能优势。从一开始就消除资源争夺,往往会带来更简单的设计和出色的性能。

线程和你的用户界面

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

有几个明显的例外,从其他线程执行图形操作是有利的。例如,你可以使用辅助线程来创建和处理图像,并执行其他与图像相关的计算。在这些操作中使用辅助线程可以大大增加性能。如果你对某一特定的图形操作不确定,请计划从你的主线程进行操作。

关于Cocoa线程安全的更多信息,请参阅Thread Safety Summary。关于Cocoa中绘图的更多信息,请看Cocoa Drawing Guide

注意退出时的线程行为

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

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

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

处理异常

异常处理机制依赖于当前的调用堆栈,在抛出异常时执行任何必要的清理。因为每个线程都有自己的调用堆栈,所以每个线程都要负责捕捉自己的异常。在次要线程中未能捕捉到一个异常与在主线程中未能捕捉到一个异常是一样的:拥有的进程被终止。你不能把一个未捕获的异常抛给另一个线程处理。

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

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

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

干净地终止你的线程

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

关于退出线程的正确方法的更多信息,请参阅Terminating a Thread.

库中的线程安全

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

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

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

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

线程管理

OS X或iOS中的每个进程(应用程序)都是由一个或多个线程组成的,每个线程代表了应用程序代码的单一执行路径。每个应用程序开始时都有一个线程,它运行应用程序的主功能。应用程序可以产生额外的线程,每个线程执行特定功能的代码。

当一个应用程序产生一个新的线程时,该线程成为应用程序进程空间中的一个独立实体。每个线程都有自己的执行栈,并由内核单独安排运行时间。一个线程可以与其他线程和其他进程进行通信,执行I/O操作,并做任何其他你可能需要它做的事情。然而,由于它们在同一个进程空间内,一个应用程序中的所有线程共享相同的虚拟内存空间,并具有与进程本身相同的访问权限。

本章概述了OS X和iOS中可用的线程技术,以及如何在你的应用程序中使用这些技术的例子。

注:关于Mac OS的线程架构的历史回顾,以及关于线程的其他背景信息,请参见技术说明TN2028,"Threading Architectures"。

线程Cost

在内存使用和性能方面,线程对你的程序(和系统)有一个真正的成本。每个线程都需要在内核内存空间和你的程序的内存空间中分配内存。管理你的线程和协调其调度所需的核心结构是使用有线内存存储在内核中。你的线程的堆栈空间和每个线程的数据被存储在你的程序的内存空间中。这些结构大部分是在你第一次创建线程时创建和初始化的--由于需要与内核进行交互,这个过程可能相对昂贵。

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

ItemApproximate costNotes
内核数据结构大约1KB该内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此不能被分页到磁盘。
堆栈空间512 KB (二级线程)8 MB (OS X主线程)1 MB (iOS主线程)二级线程允许的最小堆栈大小是16KB,堆栈大小必须是4KB的倍数。在线程创建时,该内存的空间被预留在你的进程空间中,但与该内存相关的实际页面在需要时才会创建。
创建时间大约90微秒这个值反映了从最初调用创建线程到线程的进入点例程开始执行的时间。这些数字是通过分析线程创建过程中产生的平均值和中位数来确定的,这台基于英特尔的iMac配备了2GHz酷睿双核处理器和1GB的内存

注意:因为有底层内核的支持,操作对象通常可以更快创建线程。与其每次都从头开始创建线程,不如使用已经驻留在内核中的线程池来节省分配时间。关于使用操作对象的更多信息,请参见Concurrency Programming Guide

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

创建线程

创建低级别的线程是比较简单的。在所有情况下,你必须有一个函数或方法作为你的线程的主入口,你必须使用一个可用的线程例程来启动你的线程。下面的章节展示了更常用的线程技术的基本创建过程。使用这些技术创建的线程会继承一组默认的属性,这由你使用的技术决定。关于如何配置你的线程的信息,请参阅配置线程属性。

使用NSThread

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

  1. 使用detachNewThreadSelector:toTarget:withObject: 类方法来生成新的线程。
  2. 创建一个新的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]; // 实际创建线程

注意:使用initWithTarget:selector:object:方法的另一个方法是子类化NSThread并重写其主方法。你可以使用这个方法的重载版本来实现你的线程的主入口。更多信息,请参见NSThread类参考中的子类说明。

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

注意:尽管对于线程间的偶尔通信很有好处,但对于时间紧迫或频繁的线程间通信,你不应该使用performSelector:onThread:withObject:waitUntilDone: 方法。

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

使用POSIX线程

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

清单2-1显示了两个使用POSIX调用来创建线程的自定义函数。LaunchThread函数创建了一个新的线程,其主程序在PosixThreadMainRoutine函数中实现。因为POSIX创建的线程默认是可连接的,这个例子改变了线程的属性以创建一个分离的线程。将线程标记为分离的线程,使系统有机会在线程退出时立即回收该线程的资源。

#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函数,它将在你的应用程序中创建一个新的分离线程。当然,使用这段代码创建的新线程不会做任何有用的事情。这些线程将被启动并几乎立即退出。为了使事情更有趣,你需要在PosixThreadMainRoutine函数中添加代码,以做一些实际工作。为了确保线程知道要做什么工作,你可以在创建时给它一个指向一些数据的指针。你把这个指针作为pthread_create函数的最后一个参数传给它。

为了将信息从新创建的线程传回给应用程序的主线程,你需要在目标线程之间建立一个通信路径。对于基于C的应用程序,有几种方法可以在线程之间进行通信,包括使用端口、条件或共享内存。对于长期存在的线程,你几乎总是应该建立某种线程间的通信机制,以使你的应用程序的主线程有办法检查线程的状态或在应用程序退出时干净地关闭它。

关于POSIX线程函数的更多信息,请参见pthread man页。

使用 NSObject 生成一个线程

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

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

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

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

尽管NSThread类是在Cocoa应用程序中创建线程的主要接口,但如果使用POSIX线程对你来说更方便的话,你可以自由地使用它。例如,如果你已经有了使用POSIX线程的代码,而且你不想重写它,那么你可以使用POSIX线程。如果你确实打算在Cocoa应用程序中使用POSIX线程,你仍然应该注意Cocoa和线程之间的相互作用,并遵守下面几节中的准则。

保护Cocoa框架

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

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

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

混合使用POSIX锁和Cocoa锁

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

配置线程属性

在你创建一个线程之后,有时在创建之前,你可能想配置线程环境的不同部分。下面几节描述了一些你可以做的改变,以及你何时可以做这些改变。

配置线程的堆栈大小

对于你创建的每个新线程,系统都会在你的进程空间中分配一定量的内存,作为该线程的堆栈。堆栈管理着堆栈框架,也是声明线程的任何局部变量的地方。为线程分配的内存量在线程成本中列出。

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

TechnologyOption
Cocoa在iOS和OS X v10.5及更高版本中,分配并初始化一个NSThread对象(不要使用detachNewThreadSelector:toTarget:withObject: 方法)。在调用线程对象的start方法之前,使用setStackSize:方法来指定新的堆栈大小。
POSIX创建一个新的pthread_attr_t结构并使用pthread_attr_setstacksize函数来改变默认的堆栈大小。在创建你的线程时,将属性传递给pthread_create函数。
MultiProcessing services当你创建你的线程时,将适当的堆栈大小值传递给MPCreateTask函数。

配置线程本地存储

每个线程都维护一个键值对的字典,可以从线程的任何地方访问。你可以用这个字典来存储你想在整个线程的执行过程中持续存在的信息。例如,你可以用它来存储你想在线程运行循环的多次迭代中坚持的状态信息。

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

设置一个线程的分离状态

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

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

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

如果你确实想创建可加入的线程,唯一的方法是使用POSIX线程。POSIX默认将线程创建为可连接的。要把一个线程标记为分离的或可连接的,在创建线程之前使用pthread_attr_setdetachstate函数修改线程属性。在线程开始后,你可以通过调用pthread_detach函数将一个可加入的线程改为分离的线程。关于这些POSIX线程函数的更多信息,请参见pthread man页。关于如何加入一个线程的信息,请参见pthread_join man页。

设置线程的优先级

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

重要提示:一般来说,让你的线程的优先级保持在默认值是个好主意。增加一些线程的优先级也会增加低优先级线程饿死的可能性。如果你的应用程序包含高优先级和低优先级的线程,它们必须相互作用,低优先级线程的饥饿可能会阻碍其他线程,造成性能瓶颈。

如果你确实想修改线程的优先级,Cocoa和POSIX都提供了这样的方法。对于Cocoa线程,你可以使用NSThread的setThreadPriority: 类方法来设置当前运行线程的优先级。对于POSIX线程,你可以使用pthread_setschedparam函数。更多信息,请参见NSThread类参考或pthread_setschedparam man page。

编写你的线程入口例程

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

创建一个自动释放池

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

如果一个应用程序使用垃圾收集而不是管理的内存模型,创建一个自动释放池并不是严格意义上的必要。在一个垃圾收集的应用程序中,自动释放池的存在是无害的,而且在大多数情况下会被简单地忽略。如果一个代码模块必须同时支持垃圾收集和内存管理模型,那么它是允许的。在这种情况下,自动释放池必须存在以支持托管内存模型的代码,如果应用程序在启用垃圾收集的情况下运行,自动释放池将被简单地忽略。

如果你的应用程序使用托管内存模型,创建一个自动释放池应该是你在线程进入例程中做的第一件事。同样地,销毁这个自动释放池应该是你在线程中做的最后一件事。这个池子确保自动释放的对象被捕获,尽管它在线程本身退出之前不会释放它们。清单2-2显示了一个使用自动释放池的基本线程入口例程的结构。

//清单2-2 定义你的线程进入点例程 
- (void)myThreadMainRoutine {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];// 顶层的池子
    // 在这里做线程工作。
    [pool release]; // 释放池中的对象。
}

因为顶层自动释放池在线程退出前不会释放其对象,所以长寿线程应该创建额外的自动释放池来更频繁地释放对象。例如,一个使用运行循环的线程可能会在每次通过该运行循环时创建并释放一个自动释放池。更频繁地释放对象可以防止你的应用程序的内存足迹增长过大,这可能会导致性能问题。就像任何与性能有关的行为一样,你应该测量你的代码的实际性能,并适当地调整你对自动释放池的使用。

关于内存管理和自动释放池的更多信息,请参阅Advanced Memory Management Programming Guide.

设置一个异常处理程序

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

在Xcode中构建你的项目时,你可以使用C++或Objective-C的异常处理风格。关于在Objective-C中设置如何引发和捕捉异常的信息,请参见Exception Programming Topics.

设置一个运行循环

当编写你想在一个独立的线程上运行的代码时,你有两个选择。第一个选择是把线程的代码写成一个长的任务,在很少或没有中断的情况下执行,并在完成后让线程退出。第二种选择是把你的线程放入一个循环中,让它在请求到来时动态地处理它们。第一个选项不需要对你的代码进行特殊的设置;你只需开始做你想做的工作。然而,第二种选择涉及设置你的线程的运行循环。

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

关于使用和配置运行循环的信息,请参阅Run Loops.

终止一个线程

推荐退出一个线程的方法是让它正常退出其入口点例程。尽管Cocoa、POSIX和多处理服务提供了直接杀死线程的例程,但我们强烈反对使用这种例程。杀死一个线程会使该线程无法清理自己的工作。线程分配的内存有可能被泄露,线程当前使用的任何其他资源也可能没有被正确清理,从而在以后产生潜在问题。

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

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