一、什么是线程?
线程是一种相对轻量级的方法,可以在应用程序内实现多个执行路径
。在系统级别,程序并排运行,系统根据每个程序的需求和其他程序的需求为每个程序分配执行时间
。然而,每个程序内部都存在一个或多个执行线程,可用于同时或几乎同时执行不同的任务。系统本身实际上管理这些执行线程,将它们安排在可用内核上运行,并根据需要先发制人地中断它们,以允许其他线程运行。
从技术角度来看,线程是管理代码执行所需的内核级和应用程序级数据结构的组合。内核级结构协调向线程发送事件和线程在可用内核之一上的先发制人调度。应用程序级结构包括用于存储函数调用的调用堆栈,以及应用程序管理和操作线程属性和状态所需的结构。
在非并发应用程序中,只有一个执行线程。该线程以应用程序main
例程开始和结束,并逐个分支到不同的方法或函数,以实现应用程序的整体行为。相比之下,支持并发的应用程序从一个线程开始,并根据需要添加更多内容来创建额外的执行路径。每个新路径都有自己的自定义启动例程,独立于应用程序main
例程中的代码运行。在应用程序中拥有多个线程有两个非常重要的潜在优势:
多个线程可以提高应用程序的感知响应能力。
多个线程可以提高应用程序在多核系统的实时性能。
如果您的应用程序只有一个线程,则该线程必须执行所有操作。它必须响应事件,更新应用程序的窗口,并执行实现应用程序行为所需的所有计算。只有一个线程的问题是它一次只能做一件事。那么,当您的计算需要很长时间才能完成时,会发生什么?当您的代码忙于计算所需的值时,应用程序会停止响应用户事件并更新其窗口。如果这种行为持续足够长的时间,用户可能会认为您的应用程序被挂起,并尝试强制退出。然而,如果您将自定义计算移动到单独的线程上,应用程序的主线程将可以更及时地自由响应用户交互。
由于现在多核计算机很常见,线程提供了一种提高某些类型应用程序性能的方法。执行不同任务的线程可以在不同的处理器核心上同时执行,使应用程序能够在给定的时间内增加工作量。
当然,线程不是修复应用程序性能问题的灵丹妙药。随着线程带来的好处,还带来了潜在的问题。在应用程序中拥有多个执行路径可能会使代码增加相当大的复杂性。每个线程必须与其他线程协调其操作,以防止其损坏应用程序的状态信息。由于单个应用程序中的线程共享相同的内存空间
,因此它们可以访问所有相同的数据结构。如果两个线程试图同时操作相同的数据结构,一个线程可能会以损坏生成的数据结构的方式覆盖另一个线程的更改。即使有适当的保护,您仍然需要注意编译器优化,这些优化会将微妙(而不是那么微妙)的错误引入代码。
1.线程术语
线程
用于指代码的单独执行路径。进程
用于指正在运行的可执行文件,该可执行文件可以包含多个线程。任务
一词用于指需要执行的抽象工作概念。
2.线程替代方案
Grand Central Dispatch在Mac OS x v10.6中推出,是线程的一种替代方案,允许您专注于需要执行的任务,而不是线程管理。使用GCD,您可以定义要执行的任务并将其添加到工作队列中,该队列处理在适当线程上的任务调度。工作队列考虑了可用内核的数量和当前负载,以比您自己使用线程更有效地执行任务。
3.运行循环
运行循环是用于管理异步到达线程的事件的基础设施。运行循环通过监控线程的一个或多个事件源来工作。当事件到达时,系统唤醒线程并将事件调度到运行循环,然后运行循环将它们调度到您指定的处理程序。如果没有事件并准备处理,运行循环会将线程置于睡眠状态。
您不需要对创建的任何线程使用运行循环,但这样做可以为用户提供更好的体验。运行循环可以创建使用最少资源的长寿命线程。由于运行循环在无事可做时将其线程置于睡眠状态,因此它消除了轮询的需求,轮询浪费了CPU循环,并防止处理器本身进入睡眠和节省电力。
要配置运行循环,您只需启动线程,获取对运行循环对象的引用,安装事件处理程序,并告诉运行循环运行。OS X提供的基础架构会自动为您处理主线程运行循环的配置。
有关运行循环的详细信息以及如何使用它们的示例在运行循环中提供。
4.同步工具
线程编程的危害之一是多个线程之间的资源争用
。如果多个线程试图同时使用或修改同一资源,可能会出现问题。缓解这个问题的一个方法是完全消除共享资源,并确保每个线程都有自己的一组不同的资源来操作。不过,当维护完全独立的资源不可行时,您可能不得不使用锁
、条件
、原子操作
同步对资源的访问。
锁为一次只能由一个线程执行的代码提供了一种蛮力形式的保护。最常见的锁类型是互斥锁,也称为互斥锁。当线程试图获取当前由另一个线程持有的互斥体时,它会阻止,直到另一个线程释放锁。几个系统框架为互斥锁提供支持,尽管它们都基于相同的底层技术。此外,Cocoa提供了互斥锁的几种变体,以支持不同类型的行为,如递归。有关可用锁类型的更多信息,请参阅锁。
除了锁外,系统还支持条件,确保应用程序中任务的适当排序。条件充当守门人,阻止给定的线程,直到它所代表的条件变成真。当这种情况发生时,条件会释放线程并允许它继续。POSIX层和基础框架都为条件提供了直接支持。(如果您使用操作对象,您可以在操作对象之间配置依赖项来对任务的执行进行排序,这与条件提供的行为非常相似。)
虽然锁和条件在并发设计中非常常见,但原子操作是保护和同步数据访问的另一种方式。在您可以对标量数据类型(scalar data types)(OC中基本数据类型)执行数学或逻辑操作的情况下,原子操作提供了锁的轻量级替代方案。原子操作使用特殊的硬件指令,以确保在其他线程有机会访问变量之前完成对变量的修改。
有关可用同步工具的更多信息,请参阅同步工具。
5.线程间通信
虽然良好的设计最大限度地减少了所需的通信量,但在某些时候,线程之间的通信变得必要。(线程的工作是为您的应用程序工作,但如果该工作的结果从未被使用过,它有什么好处?)线程可能需要处理新的工作请求或向应用程序的主线程报告其进度。在这些情况下,您需要一种方法来获取信息从一个线程到另一个线程。幸运的是,线程共享相同的进程空间
这一事实意味着您有很多通信选项。
线程之间有很多沟通方式,每种方式都有各自的优缺点。配置线程本地存储列出了您可以在OS X中使用的最常见的通信机制。(除消息队列外,这些技术在iOS中也可用。)本表中的技术按复杂性的增加顺序列出。
通信机制
机制 | 描述 |
---|---|
直接消息 | Cocoa应用程序支持直接在其他线程上执行选择器的能力。此功能意味着一个线程基本上可以在任何其他线程上执行方法。由于它们是在目标线程的上下文中执行的,因此以这种方式发送的消息会自动在该线程上序列化。有关输入源的信息,请参阅可可执行选择器源。 |
全局变量、共享内存和对象 | 在两个线程之间通信信息的另一种简单方法是使用全局变量、共享对象或共享内存块。虽然共享变量快速简单,但它们也比直接消息传递更脆弱。共享变量必须用锁或其他同步机制仔细保护,以确保代码的正确性。如果不这样做,可能会导致种族状况、数据损坏或崩溃。 |
条件 | 条件是一种同步工具,可用于控制线程何时执行特定部分代码。您可以将条件视为门卫,只在满足所述条件时让线程运行。有关如何使用条件的信息,请参阅使用条件。 |
运行循环源 | 自定义运行循环源是您为在线程上接收特定于应用程序的消息而设置的源。由于它们是事件驱动的,运行循环源在无事可做时自动将线程置于睡眠状态,从而提高线程的效率。有关运行循环和运行循环源的信息,请参阅运行循环。 |
端口和套接字 | $基于端口的通信是两个线程之间更复杂的通信方式,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与其他流程和服务等外部实体通信。为了提高效率,端口使用运行循环源实现,因此当端口上没有数据等待时,线程会进入睡眠状态。有关运行循环和基于端口的输入源的信息,请参阅运行循环。 |
消息队列 | 传统的多处理服务定义了用于管理传入和传出数据的先进先出(FIFO)队列抽象。虽然消息队列简单方便,但它们不如其他一些通信技术高效。有关如何使用消息队列的更多信息,请参阅多处理服务编程指南。 |
6.保持线程合理繁忙
如果您决定手动创建和管理线程,请记住线程消耗宝贵的系统资源。您应该尽最大努力确保分配给线程的任何任务都是合理的长寿和高效的。与此同时,您不应该害怕终止大部分时间闲置的线程。线程使用数量不平凡的内存,其中一些是有线的,因此释放空闲线程不仅有助于减少应用程序的内存占用空间,还可以释放更多的物理内存供其他系统进程使用。
二、线程管理
iOS中的每个进程(应用程序)都由一个或多个线程组成,每个线程代表通过应用程序代码执行的单一路径。每个应用程序都以一个线程开头,该线程运行应用程序main功能。应用程序可以生成额外的线程,每个线程都执行特定函数的代码。
当应用程序生成新线程时,该线程将成为应用程序进程空间内的独立实体。每个线程都有自己的执行堆栈,由内核单独安排运行时。线程可以与其他线程和其他进程通信,执行I/O操作,并执行您可能需要它执行的任何其他操作。然而,由于它们位于同一个进程空间内,单个应用程序中的所有线程共享相同的虚拟内存空间,并拥有与进程本身相同的访问权限。
本章概述了iOS中可用的线程技术,以及如何在应用程序中使用这些技术的示例。
1.线程成本
就内存使用和性能而言,线程对您的程序(和系统)来说是真正的成本。每个线程都需要在内核内存空间和程序的内存空间中分配内存
。管理线程和协调其调度所需的核心结构使用有线内存(wired memory)
存储在内核中。线程的堆栈空间和每个线程数据存储在程序的内存空间中。这些结构大多是在您首次创建线程时创建和初始化的——由于需要与内核交互,这个过程可能相对昂贵。
表2-1量化了在应用程序中创建新的用户级线程的大致成本。其中一些成本是可配置的,例如为辅助线程分配的堆栈空间量。创建线程的时间成本是一个粗略的近似值,只应用于彼此的相对比较。线程创建时间可能因处理器负载、计算机速度以及可用系统和程序内存量而有很大差异。
表2-1线程创建成本
项目 | 大约成本 | 笔记 |
---|---|---|
内核数据结构 | 大约1KB | 此内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此无法分页到磁盘。 |
堆栈空间 | 512 KB(次机线程) 8 MB(OS X主线程) 1 MB(iOS主线程) |
辅助线程允许的最小堆栈大小为16KB,堆栈大小必须是4KB的倍数。在线程创建时,此内存的空间将预留到您的进程空间中,但与该内存关联的实际页面要到需要时才会创建。 |
创建时间 | 大约90微秒 | 此值反映了创建线程的初始调用与线程的入口点例程开始执行之间的时间。这些数字是通过分析基于英特尔的配备2GHz Core Duo处理器和运行OS X v10.5的1 GB内存的基于英特尔的iMac上创建线程时生成的平均值和中值来确定的。 |
水果 | 价格 | 数量 |
---|---|---|
香蕉 | $1 | 5 |
苹果 | $1 | 6 |
草莓 | $1 | 7 |