iOS多线程编程指南(一)关于多线程编程(转)

原文:http://www.dreamingwish.com/article/ios-multi-threaded-programming-a-multi-threaded-programming.html

第一章      关于多线程编程

多年来,计算机的最大性能主要受限于它的中心微处理器的速度。然而因为个别处理器已经开始达到它的瓶颈限制,芯片制造商开始转向多核设计,让计算机具备了同时执行多个任务的能力。尽管Mac OS X利用了这些核心优点,在任什么时候候能够执行系统相关的任务,但本身的应用程序也能够经过多线程方法利用这些优点。html

1.1        什么是多线程

多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径。在系统级别内,程序并排执行,系统分配到每一个程序的执行时间是基于该程序的所需时间和其余程序的所需时间来决定的。然而在每一个应程序的内部,存在一个或多个执行线程,它同时或在一个几乎同时发生的方式里执行不一样的任务。系统自己管理这些执行的线程,调度它们在可用的内核上运行,并在须要让其余线程执行的时候抢先打断它们。ios

从技术角度来看,一个线程就是一个须要管理执行代码的内核级和应用级数据结构组合。内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。应用级结构包括用于存储函数调用的调用堆栈和应用程序须要管理和操做线程属性和状态的结构。编程

在非并发的应用程序,只有一个执行线程。该线程开始和结束于你应用程序的main循环,一个个方法和函数的分支构成了你整个应用程序的全部行为。与此相反,支持并发的应用程序开始能够在须要额外的执行路径时候建立一个或多个线程。每一个新的执行路径有它本身独立于应用程序main循环的定制开始循环。在应用程序中存在多个线程提供了两个很是重要的的潜在优点:安全

  1. 多个线程能够提升应用程序的感知响应。
  2. 多个线程能够提升应用程序在多核系统上的实时性能。

若是你的应用程序只有单独的线程,那么该独立程序须要完成全部的事情。它必须对事件做出响应,更新您的应用程序的窗口,并执行全部实现你应用程序行为须要的计算。拥有单独线程的主要问题是在同一时间里面它只能执行一个任务。那么当你的应用程序须要很长时间才能完成的时候会发生什么呢?当你的代码忙于计算你所须要的值的时候,你的程序就会中止响应用户事件和更新它的窗口。若是这样的状况持续足够长的时间,用户就会误认为你的程序被挂起了,并试图强制退出。若是你把你的计算任务转移到一个独立的线程里面,那么你的应用程序主线程就能够自由并及时响应用户的交互。数据结构

固然多线程并非解决程序性能问题的灵丹妙药。多线程带来好处同时也伴随着潜在问题。应用程序内拥有多个可执行路径,会给你的代码增长更多的复杂性。每一个线程须要和其余线程协调其行为,以防止它破坏应用程序的状态信息。由于应用程序内的多个线程共享内存空间,它们访问相同的数据结构。若是两个线程试图同时处理相同的数据结构,一个线程有可能覆盖另外线程的改动致使破坏该数据结构。即便有适当的保护,你仍然要注意因为编译器的优化致使给你代码产生很微妙的(和不那么微妙)的Bug。多线程

1.2        线程术语

在讨论多线程和它支持的相关技术以前,咱们有必要先了解一些基本的术语。若是你熟悉Carbon的多处理器服务API或者UNIX系统的话,你会发现本文档里面“任务(task)”被用于不一样的定义。在Mac OS的早期版本,术语“任务(task)”是用来区分使用多处理器服务建立的线程和使用Carbon线程管理API建立的线程。在UNIX系统里面,术语“任务(task)”也在一段时间内被用于指代运行的进程。在实际应用中,多处理器服务任务是至关于抢占式的线程。并发

因为Carbon线程管理器和多处理器服务API是Mac OS X的传统技术,本文件采用下列术语:app

  1. 线程(线程)用于指代独立执行的代码段。
  2. 进程(process)用于指代一个正在运行的可执行程序,它能够包含多个线程。
  3. 任务(task)用于指代抽象的概念,表示须要执行工做。

 

 

1.3        多线程的替代方法

你本身建立多线程代码的一个问题就是它会给你的代码带来不肯定性。多线程是一个相对较低的水平和复杂的方式来支持你的应用程序并发。若是你不彻底理解你的设计选择的影响,你可能很容易遇到同步或定时问题,其范围能够从细微的行为变化到严重到让你的应用程序崩溃并破坏用户数据。框架

你须要考虑的另外一个因素是你是否真的须要多线程或并发。多线程解决了如何在同一个进程内并发的执行多路代码路径的问题。然而在不少状况下你是没法保证你所在作的工做是并发的。多线程引入带来大量的开销,包括内存消耗和CPU占用。你会发现这些开销对于你的工做而言实在太大,或者有其余方法会更容易实现。异步

表1-1列举了多线程的替代方法。该表包含了多线程的替代技术(好比操做对象和GCD)和如何更高效的使用单个线程。

Table 1-1  Alternative technologies to threads

Technology

Description

Operation objects

Introduced in Mac OS X v10.5, an operation object is a wrapper for a task that would normally be executed on a secondary thread. This wrapper hides the thread management aspects of performing the task, leaving you free to focus on the task itself. You typically use these objects in conjunction with an operation queue object, which actually manages the execution of the operation objects on one more threads.
For more information on how to use operation objects, see Concurrency Programming Guide.

Grand Central Dispatch (GCD)

Introduced in Mac OS x v10.6, Grand Central Dispatch is another alternative to threads that lets you focus on the tasks you need to perform rather than on thread management. With GCD, you define the task you want to perform and add it to a work queue, which handles the scheduling of your task on an appropriate thread. Work queues take into account the number of available cores and the current load to execute your tasks more efficiently than you could do yourself using threads.
For information on how to use GCD and work queues, see Concurrency Programming Guide

Idle-time notifications

For tasks that are relatively short and very low priority, idle time notifications let you perform the task at a time when your application is not as busy. Cocoa provides support for idle-time notifications using theNSNotificationQueue object. To request an idle-time notification, post a notification to the default NSNotificationQueue object using the NSPostWhenIdle option. The queue delays the delivery of your notification object until the run loop becomes idle. For more information, see Notification Programming Topics.

Asynchronous functions

The system interfaces include many asynchronous functions that provide automatic concurrency for you. These APIs may use system daemons and processes or create custom threads to perform their task and return the results to you. (The actual implementation is irrelevant because it is separated from your code.) As you design your application, look for functions that offer asynchronous behavior and consider using them instead of using the equivalent synchronous function on a custom thread.

Timers

You can use timers on your application’s main thread to perform periodic tasks that are too trivial to require a thread, but which still require servicing at regular intervals. For information on timers, see “Timer Sources.”

Separate processes

Although more heavyweight than threads, creating a separate process might be useful in cases where the task is only tangentially related to your application. You might use a process if a task requires a significant amount of memory or must be executed using root privileges. For example, you might use a 64-bit server process to compute a large data set while your 32-bit application displays the results to the user.

注意:当使用fork函数加载独立进程的时候,你必须老是在fork后面调用exec或者相似的函数。基于Core Foundation、Cocao或者Core Data框架(不管显式仍是隐式关联)的应用程序随后调用exec函数或者相似的函数都会导出不肯定的结果。

1.4        线程支持

若是你已经有代码使用了多线程,Mac OS X和iOS提供几种技术来在你的应用程序里面建立多线程。此外,两个系统都提供了管理和同步你须要在这些线程里面处理的工做。如下几个部分描述了一些你在Mac OS X和iOS上面使用多线程的时候须要注意的关键技术。

1.4.1    线程包

虽然多线程的底层实现机制是Mach的线程,你不多(即便有)使用Mach级的线程。相反,你会常用到更多易用的POSIX 的API或者它的衍生工具。Mach的实现没有提供多线程的基本特征,可是包括抢占式的执行模型和调度线程的能力,因此它们是相互独立的。

列表1-2列举你能够在你的应用程序使用的线程技术。

Table 1-2  Thread technologies

Technology

Description

Cocoa threads

Cocoa implements threads using the NSThread class. Cocoa also provides methods on NSObject for spawning new threads and executing code on already-running threads. For more information, see “Using NSThread” and “Using NSObject to Spawn a Thread.”

POSIX threads

POSIX threads provide a C-based interface for creating threads. If you are not writing a Cocoa application, this is the best choice for creating threads. The POSIX interface is relatively simple to use and offers ample flexibility for configuring your threads. For more information, see “Using POSIX Threads”

Multiprocessing Services

Multiprocessing Services is a legacy C-based interface used by applications transitioning from older versions of Mac OS. This technology is available in Mac OS X only and should be avoided for any new development. Instead, you should use the NSThread class or POSIX threads. If you need more information on this technology, seeMultiprocessing Services Programming Guide.

    在应用层上,其余平台同样全部线程的行为本质上是相同的。线程启动以后,线程就进入三个状态中的任何一个:运行(running)、就绪(ready)、阻塞(blocked)。若是一个线程当前没有运行,那么它不是处于阻塞,就是等待外部输入,或者已经准备就绪等待分配CPU。线程持续在这三个状态之间切换,直到它最终退出或者进入中断状态。

当你建立一个新的线程,你必须指定该线程的入口点函数(或Cocoa线程时候为入口点方法)。该入口点函数由你想要在该线程上面执行的代码组成。但函数返回的时候,或你显式的中断线程的时候,线程永久中止,且被系统回收。由于线程建立须要的内存和时间消耗都比较大,所以建议你的入口点函数作至关数量的工做,或创建一个运行循环容许进行常常性的工做。

为了获取更多关于线程支持的可用技术而且如何使用它们,请阅读“线程管理部分”。

1.4.2    Run Loops

注:为了便于记忆,文本后面部分翻译Run Loops的时候基本采用原义,而非翻译为“运行循环”。

    一个run loop是用来在线程上管理事件异步到达的基础设施。一个run loop为线程监测一个或多个事件源。当事件到达的时候,系统唤醒线程并调度事件到run loop,而后分配给指定程序。若是没有事件出现和准备处理,run loop把线程置于休眠状态。

你建立线程的时候不须要使用一个run loop,可是若是你这么作的话能够给用户带来更好的体验。Run Loops可让你使用最小的资源来建立长时间运行线程。由于run loop在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU周期轮询,并防止处理器自己进入休眠状态并节省电源。

为了配置run loop,你所须要作的是启动你的线程,获取run loop的对象引用,设置你的事件处理程序,并告诉run loop运行。Cocoa和Carbon提供的基础设施会自动为你的主线程配置相应的run loop。若是你打算建立长时间运行的辅助线程,那么你必须为你的线程配置相应的run loop。

关于run loops的详细信息和如何使用它们的例子会在“Run Loops”部分介绍。

1.4.3    同步工具

线程编程的危害之一是在多个线程之间的资源争夺。若是多个线程在同一个时间试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源,并确保每一个线程都有在它操做的资源上面的独特设置。由于保持彻底独立的资源是不可行的,因此你可能必须使用锁,条件,原子操做和其余技术来同步资源的访问。

锁提供了一次只有一个线程能够执行代码的有效保护形式。最广泛的一种锁是互斥排他锁,也就是咱们一般所说的“mutex”。当一个线程试图获取一个当前已经被其余线程占据的互斥锁的时候,它就会被阻塞直到其余线程释放该互斥锁。系统的几个框架提供了对互斥锁的支持,虽然它们都是基于相同的底层技术。此外Cocoa提供了几个互斥锁的变种来支持不一样的行为类型,好比递归。获取更多关于锁的种类的信息,请阅读“锁”部份内容。

除了锁,系统还提供了条件,确保在你的应用程序任务执行的适当顺序。一个条件做为一个看门人,阻塞给定的线程,直到它表明的条件变为真。当发生这种状况的时候,条件释放该线程并容许它继续执行。POSIX级别和基础框架都直接提供了条件的支持。(若是你使用操做对象,你能够配置你的操做对象之间的依赖关系的顺序肯定任务的执行顺序,这和条件提供的行为很是类似)。

尽管锁和条件在并发设计中使用很是广泛,原子操做也是另一种保护和同步访问数据的方法。原子操做在如下状况的时候提供了替代锁的轻量级的方法,其中你能够执行标量数据类型的数学或逻辑运算。原子操做使用特殊的硬件设施来保证变量的改变在其余线程能够访问以前完成。

获取更多关于可用同步工具信息,请阅读“同步工具”部分。

1.4.4    线程间通讯

虽然一个良好的设计最大限度地减小所需的通讯量,但在某些时候,线程之间的通讯显得十分必要。(线程的任务是为你的应用程序工做,但若是历来没有使用过这些工做的结果,那有什么好处呢?)线程可能须要处理新的工做要求,或向你应用程序的主线程报告其进度状况。在这些状况下,你须要一个方式来从其余线程获取信息。幸运的是,线程共享相同的进程空间,意味着你能够有大量的可选项来进行通讯。

线程间通讯有不少种方法,每种都有它的优势和缺点。“配置线程局部存储”列出了不少你能够在Mac OS X上面使用的通讯机制。(异常的消息队列和Cocoa分布式对象,这些技术也可在iOS用来通讯)。本表中的技术是按照复杂性的顺序列出。

Table 1-3  Communication mechanisms

Mechanism

Description

Direct messaging

Cocoa applications support the ability to perform selectors directly on other threads. This capability means that one thread can essentially execute a method on any other thread. Because they are executed in the context of the target thread, messages sent this way are automatically serialized on that thread. For information about input sources, see “Cocoa Perform Selector Sources.”

Global variables, shared memory, and objects

Another simple way to communicate information between two threads is to use a global variable, shared object, or shared block of memory. Although shared variables are fast and simple, they are also more fragile than direct messaging. Shared variables must be carefully protected with locks or other synchronization mechanisms to ensure the correctness of your code. Failure to do so could lead to race conditions, corrupted data, or crashes.

Conditions

Conditions are a synchronization tool that you can use to control when a thread executes a particular portion of code. You can think of conditions as gate keepers, letting a thread run only when the stated condition is met. For information on how to use conditions, see “Using Conditions.”

Run loop sources

A custom run loop source is one that you set up to receive application-specific messages on a thread. Because they are event driven, run loop sources put your thread to sleep automatically when there is nothing to do, which improves your thread’s efficiency. For information about run loops and run loop sources, see “Run Loops.”

Ports and sockets

Port-based communication is a more elaborate way to communication between two threads, but it is also a very reliable technique. More importantly, ports and sockets can be used to communicate with external entities, such as other processes and services. For efficiency, ports are implemented using run loop sources, so your thread sleeps when there is no data waiting on the port. For information about run loops and about port-based input sources, see“Run Loops.”

Message queues

The legacy Multiprocessing Services defines a first-in, first-out (FIFO) queue abstraction for managing incoming and outgoing data. Although message queues are simple and convenient, they are not as efficient as some other communications techniques. For more information about how to use message queues, see Multiprocessing Services Programming Guide.

Cocoa distributed objects

Distributed objects is a Cocoa technology that provides a high-level implementation of port-based communications. Although it is possible to use this technology for inter-thread communication, doing so is highly discouraged because of the amount of overhead it incurs. Distributed objects is much more suitable for communicating with other processes, where the overhead of going between processes is already high. For more information, seeDistributed Objects Programming Topics.

1.1        设计技巧

如下各节帮助你实现本身的线程提供了指导,以确保你代码的正确性。部分指南同时提供如何利用你的线程代码得到更好的性能。任何性能的技巧,你应该在你更改你代码以前、期间、以后老是收集相关的性能统计数据。

1.1.1    避免显式建立线程

手动编写线程建立代码是乏味的,并且容易出现错误,你应该尽量避免这样作。Mac OS X和iOS经过其余API接口提供了隐式的并发支持。你能够考虑使用异步API,GCD方式,或操做对象来实现并发,而不是本身建立一个线程。这些技术背后为你作了线程相关的工做,并保证是无误的。此外,好比GCD和操做对象技术被设计用来管理线程,比经过本身的代码根据当前的负载调整活动线程的数量更高效。 关于更多GCD和操做对象的信息,你能够查阅“并发编程指南(Concurrency Programming Guid)”。

1.1.2    保持你的线程合理的忙

若是你准备人工建立和管理线程,记得多线程消耗系统宝贵的资源。你应该尽最大努力确保任何你分配到线程的任务是运行至关长时间和富有成效的。同时你不该该惧怕中断那些消耗最大空闲时间的线程。线程使用一个平凡的内存量,它的一些有线,因此释放一个空闲线程,不只有助于下降您的应用程序的内存占用,它也释放出更多的物理内存使用的其余系统进程。线程占用必定量的内存,其中一些是有线的,因此释放空闲线程不但帮助你减小了你应用程序的内存印记,并且还能释放出更多的物理内存给其余系统进程使用。

重要:在你中断你的空闲线程开始以前,你必须老是记录你应用程序当前的性能基线测量。当你尝试修改后,采起额外的测量来确保你的修改实际上提升了性能,而不是对它操做损害。

1.1.3    避免共享数据结构

避免形成线程相关资源冲突的最简单最容易的办法是给你应用程序的每一个线程一份它需求的数据的副本。当最小化线程之间的通讯和资源争夺时并行代码的效果最好。

建立多线程的应用是很困难的。即便你很是当心,而且在你的代码里面全部正确的地方锁住共享资源,你的代码依然可能语义不安全的。好比,当在一个特定的顺序里面修改共享数据结构的时候,你的代码有可能遇到问题。以原子方式修改你的代码,来弥补可能随后对多线程性能产生损耗的状况。把避免资源争夺放在首位一般能够获得简单的设计一样具备高性能的效果。

1.1.4    多线程和你的用户界面

若是你的应用程序具备一个图形用户界面,建议你在主线程里面接收和界面相关的事件和初始化更新你的界面。这种方法有助于避免与处理用户事件和窗口绘图相关的同步问题。一些框架,好比Cocoa,一般须要这样操做,可是它的事件处理能够不这样作,在主线程上保持这种行为的优点在于简化了管理你应用程序用户界面的逻辑。

有几个显著的例外,它有利于在其余线程执行图形操做。好比,QuickTime API包含了一系列能够在辅助线程执行的操做,包括打开视频文件,渲染视频文件,压缩视频文件,和导入导出图像。相似的,在Carbon和Cocoa里面,你可使用辅助线程来建立和处理图片和其余图片相关的计算。使用辅助线程来执行这些操做能够极大提升性能。若是你不肯定一个操做是否和图像处理相关,那么你应该在主线程执行这些操做。

关于QuickTime线程安全的信息,查阅Technical Note TN2125:“QuickTime的线程安全编程”。关于Cocoa线程安全的更多信息,查阅“线程安全总结”。关于Cocoa绘画信息,查阅Cocoa绘画指南(Cocoa Drawing Guide)。

1.1.5    了解线程退出时的行为

进程一直运行直到全部非独立线程都已经退出为止。默认状况下,只有应用程序的主线程是以非独立的方式建立的,可是你也可使用一样的方法来建立其余线程。当用户退出程序的时候,一般考虑适当的当即中断全部独立线程,由于一般独立线程所作的工做都是是可选的。若是你的应用程序使用后台线程来保存数据到硬盘或者作其余周期行的工做,那么你可能想把这些线程建立为非独立的来保证程序退出的时候不丢失数据。

以非独立的方式建立线程(又称做为可链接的)你须要作一些额外的工做。由于大部分上层线程封装技术默认状况下并无提供建立可链接的线程,你必须使用POSIX API来建立你想要的线程。此外,你必须在你的主线程添加代码,来当它们最终退出的时候链接非独立的线程。更多有关建立可链接的线程信息,请查阅“设置线程的脱离状态”部分。

若是你正在编程Cocoa的程序,你也能够经过使用applicationShouldTerminate:的委托方法来延迟程序的中断直到一段时间后或者完成取消。当延迟中断的时候,你的程序须要等待直到任何周期线程已经完成它们的任务且调用了replyToApplicationShouldTerminate:方法。关于更多这些方法的信息,请查阅NSApplication Class Reference。

1.1.6    处理异常

当抛出一个异常时,异常的处理机制依赖于当前调用堆栈执行任何须要的清理。由于每一个线程都有它本身的调用堆栈,因此每一个线程都负责捕获它本身的异常。若是在辅助线程里面捕获一个抛出的异常失败,那么你的主线程也一样捕获该异常失败:它所属的进程就会中断。你没法捕获同一个进程里面其余线程抛出的异常。

若是你须要通知另外一个线程(好比主线程)当前线程中的一个特殊状况,你应该捕捉异常,并简单地将消息发送到其余线程告知发生了什么事。根据你的模型和你正在尝试作的事情,引起异常的线程能够继续执行(若是可能的话),等待指示,或者干脆退出。

注意:在Cocoa里面,一个NSException对象是一个自包含对象,一旦它被引起了,那么它能够从一个线程传递到另一个线程。

在一些状况下,异常处理多是自动建立的。好比,Objective-C中的@synchronized包含了一个隐式的异常处理。

1.1.7    干净地中断你的线程

线程天然退出的最好方式是让它达到其主入口结束点。虽然有很多函数能够用来当即中断线程,可是这些函数应仅用于做为最后的手段。在线程达到它天然结束点以前中断一个线程阻碍该线程清理完成它本身。若是线程已经分配了内存,打开了文件,或者获取了其余类型资源,你的代码可能没办法回收这些资源,结果形成内存泄漏或者其余潜在的问题。

关于更多正确退出线程的信息,请查阅“中断线程”部分。

1.1.8    线程安全的库

虽然应用程序开发人员控制应用程序是否执行多个线程,类库的开发者则没法这样控制。当开发类库时,你必须假设调用应用程序是多线程,或者多线程之间能够随时切换。所以你应该老是在你的临界区使用锁功能。

对类库开发者而言,只当应用程序是多线程的时候才建立锁是不明智的。若是你须要锁定你代码中的某些部分,早期应该建立锁对象给你的类库使用,更好是显式调用初始化类库。虽然你也可使用静态库的初始化函数来建立这些锁,可是仅当没有其余方式的才应该这样作。执行初始化函数须要延长加载你类库的时间,且可能对你程序性能形成不利影响。

注意:永远记住在你的类库里面保持锁和释放锁的操做平衡。你应该老是记住锁定类库的数据结构,而不是依赖调用的代码提供线程安全环境。

若是你真正开发Cocoa的类库,那么当你想在应用程序变成多线程的时候收到通知的话,你能够给NSWillBecomeMultiThreadedNotification 注册一个观察者。不过你不该用依赖于这些收到的通知,由于它们可能在你的类库被调用以前已经被发出了。

相关文章
相关标签/搜索