当应用程序生成一个新线程时,该线程成为应用程序进程空间中的一个独立实体。每个线程都有自己的执行栈,并由内核分别计划在运行时运行。
一个线程可以与其他线程和其他进程通信,执行I/O操作,以及执行您可能需要它执行的任何其他操作。然而,由于它们位于同一进程空间内,单个应用程序中的所有线程共享相同的虚拟内存空间,并且具有与进程本身相同的访问权限。
线程成本(Thread Costs)
- 线程在
内存使用和性能方面给程序(和系统)带来了实际成本。每个线程都需要在内核内存空间和程序的内存空间中分配内存。
管理线程和协调其调度所需的核心结构 使用wired memory(有线内存)存储在内核中。
线程的stack space(栈空间) 和 每个线程的数据 存储在program’s memory space(程序的内存空间中)。大多数这些结构都是在你首次创建线程时创建和初始化的,由于需要与内核进行交互,这个过程可能相对昂贵。
如下表量化了与在应用程序中创建新的用户级线程相关的大约成本。其中一些成本是可配置的,例如为辅助线程分配的堆栈空间量。创建线程的时间成本是一个粗略的近似值,仅应用于彼此的相对比较。线程创建时间可能因处理器负载、计算机速度以及可用系统和程序内存量而有很大差异。
| 项 | 大约成本 | 备注 |
|---|---|---|
| Kernel data structures | 大约1KB | 此内存用于存储线程数据结构和属性,其中大部分作为有线内存(wired memory)分配,因此无法分页到磁盘。 |
| Stack space | 512KB(子线程); 8MB(Mac OS主线程); 1MB(iOS主线程) |
子线程允许的最小stack size为16kb,stack size必须为4kb的倍数。在线程创建时,此内存空间会在进程空间中预留,但是与该内存相关联的实际页面直到需要时才创建。 |
| Creation time | 约90微秒 | 此值反映了创建线程的初始调用到线程的入口点例程开始执行之间的时间。这些数字是通过分析在基于Intel的iMac上创建线程时生成的平均值和中间值确定的,iMac上有一个2GHz的酷睿双核处理器和运行OSXV10.5的1GB内存。 |
注意:由于具有底层内核支持,
operation objects通常可以更快地创建线程。它们不是每次都从头创建线程,而是使用内核中已经存在的线程池(pools of threads),以节省分配时间。参见并发编程指南Concurrency Programming Guide。
- 编写线程代码时要考虑的另一个成本是
production costs(生产成本)。
设计threaded application(线程化应用程序) 有时需要对 组织应用程序数据结构的方式 进行根本性的更改。进行这些更改可能是必要的,以避免使用同步,同步本身可能会对设计不良的应用程序造成巨大的性能损失。设计这些数据结构,调试线程代码中的问题,可能会增加开发 threaded application 所需的时间。但是,如果你的线程花费太多时间在等待locks或不执行任何操作,避免这些成本(即,开发threaded application所需的时间)可能会在运行时造成更大的问题。
线程技术的使用
创建low-level threads相对简单。在任何情况下,都必须有一个函数或方法作为线程的主要入口点,并且必须使用一个可用的thread routines(线程例程)来启动thread。
NSThread
如果希望在自己的执行线程中运行Objective-C方法,请使用 NSThread,适用于iOS 2.0+。NSThread类支持类似于NSOperation的语义,用于监视线程的运行时条件。可以使用这些语义来取消线程的执行,或者确定线程是否仍在执行或已完成其任务。
可以子类化NSThread并重写main方法来实现线程的main入口点。。如果重写了main,则不需要在main方法中调用super来调用继承的行为。
如下有两种使用NSThread类创建线程的方法:
- 方法一:
NSThread的类方法;
1 | // 此方式创建线程,只能在线程运行后访问一些线程属性 |
- 方法二:实例化
NSThread对象,并调用其start方法;
1 | // 此方式无需立即生成相应的新线程,使得可以在启动线程之前获取和设置各种线程属性。 |
- iOS 10.0及以上版本,都提供了以闭包的方式创建线程
1 | Thread.detachNewThread { |
- 子类化 Thread
1 | class MyThread: Thread { |
上面两种方式都会在应用程序创建一个detached thread(独立的线程),detached thread意味着线程退出时,系统会自动回收线程的资源。这也意味着您的代码以后不必显式地与线程连接。
使用 performSelector 向正在运行的线程发送消息,该线程的 run loop 必须处于活动状态。
1 | self.perform(#selector(addAction(_:)), on: thread, with: "add task", waitUntilDone: false) |
使用NSObject生成线程
在iOS和OS X v10.5及更高版本中,所有对象都可以生成新线程并使用它执行其方法。使用如下所示,使用此方法与Thread的detachNewThreadSelector效果相同,立即使用默认配置生成新线程并开始运行。
1 | self.performSelector(inBackground: #selector(runAction(_:)), with: "创建thread") |
线程的属性
在创建线程之后,有时甚至在创建线程之前,您可能需要配置线程环境的不同部分。以下各节描述了可以进行的一些更改以及何时进行更改。
配置 Stack Size
对于你创建的每个新线程,系统都会在你的进程空间中分配特定数量的内存,作为该线程的栈(stack)。栈(stack)管理栈帧(stack frames),也是线程的任何局部变量声明的地方。
如果要更改给定线程的stack size,则必须在创建线程之前进行更改。所有线程技术都提供了设置堆栈大小的某种方法,如下对于NSThread的设置:
1 | // 设置线程堆栈的大小,以kb为单位,必须为4的倍数。 |
配置线程本地存储
每个线程都维护一个键值对字典,可以从线程中的任何位置访问该字典。可以使用此字典来存储希望在整个线程执行过程中保持的信息。例如:使用它存储要在线程 run loop 的多次迭代中保留的状态信息。如下对于NSThread的设置:
1 | // 可以向该对象添加线程所需的任何键 |
设置线程的 Detached State
默认情况下,大多数高级线程技术都会创建detached threads。在大多数情况下,detached threads是首选的,因为它们允许系统在线程完成后立即释放线程的数据结构。
detached threads也不需要与程序进行显式交互。从线程中检索结果的方法由您自行决定。相比之下,系统不会为可连接线程(joinable threads)回收资源,直到另一个线程显式连接该线程,这个进程可能阻塞执行连接的线程。
可以将joinable threads看作child threads,尽管它们仍然作为独立线程运行,但 joinable thread 必须由另一个线程连接,然后系统才能回收其资源。joinable thread 还提供了一种将数据从退出线程传递到另一个线程的显式方法。在连接退出之前,joinable thread可以将数据指针或其他返回值传递给pthread_exit函数。然后,另一个线程可以通过调用pthread_join函数来声明此数据。
注意:
在应用程序退出时,detached threads可以立即终止,但joinable threads不能。在允许进程退出之前,必须连接每个joinable threads。因此,在线程正在做一些不应中断的关键工作时,例如将数据保存到磁盘时,joinable threads可能更可取。如果您确实想创建
joinable threads,那么唯一的方法就是使用POSIX线程。
设置 Thread Priority
创建的任何新线程都有一个与之关联的默认优先级。内核的调度算法在确定要运行的线程时会考虑线程 Priority,优先级较高的线程比优先级较低的线程更有可能运行。较高的优先级不能保证线程有特定的执行时间,只是与较低优先级的线程相比,调度程序更有可能选择它。
注意:通常最好将线程的优先级保留为默认值。增加某些线程的优先级也会增加
lower-priority threads之间出现饥饿的可能性。如果您的应用程序包含必须相互交互的high-priority和low-priority线程,则lower-priority threads的饥饿可能会阻塞其他线程并造成性能瓶颈。
编写 Thread Entry Routine
在大多数情况下,线程的entry point routines(入口点例程)会做一些:初始化数据结构,做一些工作或有选择地设置run loop,并在线程代码完成后进行清理。根据您的设计,在编写entry routine时可能需要采取一些额外的步骤。
创建 Autorelease Pool
在 Objective-C 框架中链接的应用程序通常必须在其每个线程中至少创建一个Autorelease Pool。如果应用程序使用managed model(托管模型)——应用程序处理对象的保留和释放——Autorelease Pool会捕获从该线程自动释放的任何对象。
如果应用程序使用垃圾收集(garbage collection)而不是托管内存模型(managed memory model),则不需要创建Autorelease Pool。如果代码模块必须同时支持garbage collection和managed memory model的情况下,必须存在Autorelease Pool以支持managed memory model代码。
如果您的应用程序使用managed memory model,创建Autorelease Pool应该是您在线程入口例程中所做的第一件事。同样,销毁这个Autorelease Pool应该是你在线程中做的最后一件事。此pool确保捕获自动释放的对象,尽管在线程本身退出之前不会释放它们。
1 | - (void)threadRoutine { |
因为top-level autorelease pool在线程退出之前不会释放其对象,所以long-lived threads(长寿命线程)应该创建额外的autorelease pools来更频繁地释放对象。例如,使用run loop的线程可能会每次通过该run loop创建和释放自动释放池。更频繁地释放对象可以防止应用程序的内存占用过大,从而导致性能问题。但是,与任何与性能相关的行为一样,您应该度量代码的实际性能,并适当地调整自动释放池的使用。
有关内存管理和自动释放池,请查看Advanced Memory Management Programming Guide.
对于 MRC 我们需要按照上面的方式创建线程的 Autorelease Pool,在 ARC 环境下,我们可以看到线程会对它入口函数中的对象进行强引用,如下所以:
1 | class Dog: NSObject { |
设置异常处理程序 Exception Handler
如果您的应用程序catches and handles exceptions(捕获并处理了异常),则应准备好线程代码以捕获可能发生的任何异常。虽然最好在异常可能发生的地方处理异常,但如果未能在线程中捕获抛出的异常,则会导致应用程序退出。
在thread entry routine中安装 final try/catch可以捕获任何未知异常并提供适当的响应。有关在Objective-C中设置如何引发和捕获异常的信息,请参阅Exception Programming Topics
设置Run Loop
在编写要在单独线程上运行的代码时,您有两个选项。
第一种选择是将线程的代码编写成一个长任务,以在很少或没有中断的情况下执行,并在线程完成后退出;第二个选项是将线程放入loop,并在请求到达时动态处理请求。第一个选项不需要为代码进行特殊设置,你只需开始做你想做的工作。然而,第二个选项涉及设置线程的run loop。
有关使用和配置运行循环的信息,请参阅Run Loop。
终止线程Terminating a Thread
退出线程的推荐方法是让线程正常退出它的entry point routine。尽管Cocoa、POSIX和Multiprocessing Services提供了直接杀死线程的routines,但强烈反对使用这些例程。终止线程会阻止该线程在其自身之后进行清理。线程分配的内存可能会泄漏,线程当前使用的任何其他资源可能无法正常清理,从而在以后造成潜在问题。
如果预计需要在操作过程中终止线程,则应从一开始就设计线程以响应取消或退出消息。对于长时间运行的操作,这可能意味着要定期停止工作并检查是否收到了此类消息。如果确实有消息要求线程退出,则该线程将有机会执行所需的清理并正常退出;否则,它只需返回工作并处理下一块数据。
响应cancel messages的一种方法是使用run loop input source来接收此类消息。如下示例显示了该代码在线程的主入口例程(main entry routine)中的结构(该示例仅显示了主循环部分,并且不包括设置autorelease pool或配置要执行的实际工作的步骤。)该示例在run loop上安装了一个custom input source,该输入源可能可以从另一个线程发送消息。在完成全部工作量的一部分后,线程会短暂运行run loop,以查看消息是否到达输入源。如果不是,则run loop立即退出,循环继续进行下一个工作块。因为处理程序无法直接访问exitNow局部变量,所以退出条件通过线程字典中的键值对传达。
1 | - (void)threadMainRoutine |