多线程编程指南(官方文档) iPhone

http://www.cocoachina.com/bbs/read.php?tid-53287-keyword-%B6%E0%CF%DF%B3%CC.html


多线程编程指南

简介

1. 关于多线程编程
多年以来,计算机的性能在很大程度上被单核处理器的速度所限制。在当前技术下,单核处理器的速度已经到达某种极限,因此,芯片制造商们转而专注于多核设计,以使计算机可以同时执行多个任务。Mac OS X 可以利用多核计算,更好的执行系统相关的任务。而开发人员也可以通过线程提高自己程序的性能。

1) 什么是线程?

线程是在程序内部运行多个流程的轻量单位。在系统级别上,程序使用系统提供的执行时间,各自运行。然而,在每个程序内部,存在着一个或多个被执行的线程,这些线程可以在同一时刻(或将近同时)完成不同的任务。由操作系统本身管理这些线程的执行,例如为线程分配执行时间和执行的内核,或是中断线程来使其他线程有机会执行。

从技术角度来看,线程是“内核级”和“程序级”数据结构的集合,用来管理代码的执行。内核级的数据结构主要处理针对线程的事件,安排线程在可用的内核上执行。而程序级的结构包括调用栈(可以保存函数调用),还有用来管理和计算线程属性和状态的数据。

在非并行程序中,只有一个线程的执行。线程起始于main函数,结束于main函数。在main函数内部,程序一句一句的执行。相比较而言,支持并行的程序起始于一个线程,然后在添加其他线程,从而创建了额外的执行路径。每个这样的路径都拥有自己的执行过程,而和程序中的主线程并行不悖。在程序中加入多线程,会为你带来以下两个非常重要的好处:

1. 多线程可以提高程序的反应速度。
2. 在多核系统上,多线程可以增强程序的实时处理能力。

如果你的程序只有一个线程,这一个线程就得做所有事情。它必须响应事件、更新程序窗口、执行所有的运算工作。单线程的问题是在特定时刻内,它只能处理一件事。那么如果我们的某些计算工作需要耗费很长时间,会发生什么?当代码忙于计算需要的值时,程序就停止对用户事件的响应,也不会再对窗口进行更新。如果时间很长的话,用户可能以为程序卡住了,强行关闭程序。但是,如果你将需要的计算工作放到一个独立的线程中去,你的主线程就有足够的时间去响应用户事件。

在多核电脑日益普及的今天,线程提高了某些程序的性能。执行多个任务的线程可以使用不同的处理器内核,从而使增加给定时间内完成的工作量成为可能。

但是,线程并不是改善程序性能问题的万金油。伴随着线程提供的好处,更多的潜在问题也随之产生。在程序中加入多个执行路径会大大增加代码的复杂度。每个线程都需要协调各自的行为,来防止弄乱程序的状态。这是因为位于同一程序的所有线程共享同一个内存空间,它们对所有的这些数据结构都有访问权限。如果两个线程同时计算一份数据,其中一个线程可能将另外线程修改的内容覆盖,那么计算结果就乱套了。即使是使用了恰当的保护方式,我们还是必须注意编译器优化选项(它可能导致代码出现很微妙的错误)。

2)线程相关术语
在深入研究线程和相关技术之前,必须先了解一些基本术语的定义。
如果你熟悉Carbon框架中的多核处理服务接口(Multiprocessor Services API),或是熟悉Unix系统,你将会发现术语“任务”(task)在本文档中的含义略有不同。在Mac OS的早期版本中,术语“任务”被用来区别使用Multiprocessor Services创建的和使用Carbon Thread Manager创建的线程。而在Unix系统之上,术语“任务”有时指的是运行中的进程(process)。实际情况中,一个Multiprocessor Services的任务和一个抢占式线程是一样的(注:没有深入的用过Carbon,有不妥之处还望指正)。

考虑到Carbon Thread Manager和Multiprocessor Services的API都是在Mac OS系统上的遗留技术,本文档遵循以下的术语规范:
1. 术语线程(thread)指独立的代码执行路径。
2. 术语进程(process)指程序的运行,进程可以包括多个线程。
3. 术语任务(task)指的是一份可以被执行的工作,是个抽象概念。

3)线程备选方案
自己创建线程时的一个问题是:它们为你的代码增加了不确定因素。线程是让程序支持并行操作的一种相对低层次的方式,而且比较复杂。如果你没有完全理解自己针对多线成设计的初衷,就很容易遇到同步或时间控制的问题。轻者改变程序的行为,严重的会造成程序崩溃或是弄乱用户的数据。

另一个需要考虑的因素是:你是否真的需要并行操作?线程可以解决在同一进程中同时执行多段代码的问题。在某些情况下,一些工作并不能保证被同时执行。线程可能带来更多的负荷,有内存消耗方面的,抑或是占用CPU时间。你需要研究是否值得为某个任务承担这样的负荷,或者使用其他简单的执行方式。

Table 1-1 列出了可供选择的线程实现方式。表中包含了线程的替代技术(操作对象-- operation object 和Grand central Dispatch),还包含了旨在提高效率的单线程技术。

Table 1-1  Alternative technologies to threads
Technology   Description
Operation objects| Mac OS X v10.5后被引入,一个操作对象是一项任务的封装,可以被子线程们来执行。使用这样的封装技术,可以忽略线程管理方面的麻烦,使程序员专注于任务本身。通常情况下,我们搭配使用操作队列对象(operation queue object)来使用操作对象。操作队列对象会管理操作对象在多线程中的执行。具体内容参考Concurrency Programming Guide.

Grand Central Dispatch |Mac OS X v10.6后被引入,Grand Central Dispatch是让我们专注于任务本身而非线程管理的另外的选择。使用Grand Central Dispatch,我们定义一项任务,并将其加入到一个工作队列中,队列会负责在合适的线程中执行任务。队列还会查看可用内核的数量和当前任务负载,从而比自己使用线程更高效的执行任务。

Idle-time notifications |对于那些相对来说较小的、优先级低的任务,“空闲时间通知”的方式可以让你在程序空闲的时候执行它们。Cocoa用NSNotificationQueue对象提供空闲时通知功能。要请求空闲时通知,就提交通知对象到默认的NSNotificationQueue(通知队列)。队列会延迟提交通知对象,直到run loop空闲。更多信息请查看Notification Programming Topics.

待续。。。每天会持续更新。

Asynchronous functions |系统接口包括了很多异步函数,这些函数本身就自动支持并行操作。它们可能使用系统守护进程来创建自己的线程、执行任务并返回结果(具体的实现方式并不重要,因为它和你的代码是分开的)。在设计程序的时候,先查查这一类支持异步操作的函数,而尽量少在自己定义的线程中用等效的同步函数。

Timers |当遇到某些需要定时执行的小任务,而且这些任务并没有动用线程的必要时,可以考虑在主线程中使用定时器。详细信息请查看“Timer Sources.”

Separate processes |尽管进程相对于线程来说有些“重量级”,如果某项任务和程序无直接关系,创建独立的进程还是有必要的。例如:任务需要申请大量的内存空间或者必须使用root权限执行。我们还可能需要使用64位的服务器进程来计算大数据,而用32位的程序显示运行结果。

4)线程的系统支持
如果你要将线程加入到现有的代码中去,Mac OS和iOS提供了几种创建线程的技术。另外,这两个系统还为管理和同步线程内的工作提供了支持。接下来的内容描述了几种在Mac OS和iOS上使用线程的关键技术。

Listing 2-2列出了在程序中可能用到的线程技术

Table 1-2  Thread technologies
Technology
Description
Cocoa threads |Cocoa使用NSThread类来实现线程。Cocoa还为NSObject类提供了一些方法来在已有线程中生成新的线程并执行代码。更多信息,请参考“Using NSThread” and “Using NSObject to Spawn a Thread.”

POSIX threads |POSIX线程技术提供了一系列基于C的接口来创建线程。如果你不打算编写Cocoa的程序,那么它是最好的选择。POSIX的接口相对简单易用且扩展性良好,便于自定义线程。更多信息,请参考“Using POSIX Threads”

Multiprocessing Services |Multiprocessing Services是老式的C接口。被用于支持Mac OS较早的版本。这项技术在Mac OS X上可用,但在新的开发项目中应该避免使用,而应使用NSThread和POSIX的线程技术。更多信息,请参考Multiprocessing Services Programming Guide。

从程序的层面上来看,所有线程的行为在本质上应该是一样的——无论是任何的平台。启动线程后,线程会有这几种运行状态:正在运行(running),准备就绪(ready)和被阻塞(blocked)。如果一个线程当前未运行,那么它可能是被阻塞,或者是在等待输入,也有可能已经就绪,但未被安排执行(scheduled)。线程会在这几个状态之间来来回回。直到真正执行完毕退出,进入终结(terminated)状态。

当你创建一个新的线程时,你必须为其指定入口函数(在Cocoa中,叫入口方法(method))。入口函数包括你想要在线程中执行的代码。当函数返回,或当你显式的终结线程时,线程就永远结束了,然后会被系统回收。线程在内存和时间片占用方面代价昂贵,正因为如此,建议你在入口函数中完成大量的工作,或者自己设置一个运行回路(run loop)来循环执行工作。

多线程可用技术的信息,请参考“Thread Management.”

运行回路(Run Loops)

运行回路是线程中管理异步事件获取的基础。它的工作是为线程监视事件源(event sources)。当事件到来之时,系统唤醒线程并将事件发送给运行回路,然后在传递给你指定的处理者。如果没有需要处理的事件到来,运行回路将线程置于休眠状态。

在线程中使用运行回路不是必须的。但是如果这样做的话,会有更好的用户体验。使用运行回路可以创建长期存在的线程,并尽可能少的占用资源。因为运行回路在无事可做的时候让线程休眠。它减少了轮询(polling)的需要,而轮询会浪费CPU循环数,阻止CPU休眠,从而比较耗电。

要设置一个运行回路,你所要做的只是运行你的线程,获得一个运行回路的对象,“安装”你的事件处理者,并且告诉运行回路去运行。不管是Cocoa还是Carbon都在主线程中自动为你提供了默认运行回路的设置。如果你需要创建一个长期存在的子线程,就需要自己来在线程里定义运行回路了。

关于运行回路的详细信息和例子,请参考“Run Loops.”。


同步工具

多线程编程中的一个大麻烦是线程间的资源争夺。如果多个线程同时修改一份资源,问题就来了。减少问题的方法之一就是减少共享资源,保证每个线程都有自己的一份资源可以使用。当然,管理完全独立的资源是不可能的,因此,我们要用到锁(locks),条件控制(conditions),原子操作(atomic operations)等技术自己控制线程对资源同步的访问。

“锁”为代码提供了“暴力”的保护方式,在同一时刻,代码只能被一个线程执行。其中,“互斥锁”是最常用的一种形式,也被叫做互斥体(mutex)。当一个线程试图获取被其他线程占用的互斥体时,该线程会被阻塞,直到互斥体被释放。几个系统框架提供对互斥体的支持,而它们都基于同样的底层技术。Cocoa提供额外的几种互斥体来支持不同的操作,比如递归操作。更多信息,请参考“Locks.”

除了锁以外,系统支持条件控制。它可以保证程序中任务按照正确的顺序执行。条件控制就像一个“门卫”,它会一直阻塞某个线程。直到特定的条件为真,才允许线程继续执行。POSIX层和Foundatoin框架都直接支持条件控制。(如果你使用操作对象(operation object),你可以设置它们之间的依赖关系,从而起到设置任务操作顺序的目的,这和条件控制很类似)。

并行程序设计中,还可以使用“原子操作”(atomic operation)来保护、同步对数据的访问。原子操作提供了一种轻量级的锁定方案,在对某些标量数据进行数学运算或逻辑运算的时候,就可以使用原子操作。原子操作会使用特定的硬件指令来保证对于某个变量的修改完成后其他的线程才能继续访问。

关于同步工具的详细信息,请查看“Synchronization Tools.”

线程间通信
虽然好的设计会尽可能减少线程间的通信,但是在某些情况下,线程间的通信是必要的(线程的任务就是为程序工作,但是如果线程的运行结果无法被使用,会有什么影响?)线程可能需要执行新的工作请求或是向程序主线程回报工作进度。在这些情况下,你就需要把一个线程的信息传递给另外一个线程。幸运的是,程序中的线程共享同一个进程空间,这意味着你有很多种通信方案。

线程间通信有很多方式,各有优劣,“Configuring Thread-Local Storage” 表中罗列了在Mac OS X上常用到的通信机制(除了消息队列(message queue)和Cocoa分布式对象(Cocoa distributed object)外,其他也可以在iOS上通用)。详细列表如下:

Table 1-3  Communication mechanisms
Direct messaging|Cocoa程序支持直接在其他线程中执行方法(perform selector)。这个功能意味着一个线程可以直接在另外一个线程中执行方法。因为要在目标线程中执行,通过这种方法发送的消息会在线程中被自动序列化。关于input sources的详细信息,请查看“Cocoa Perform Selector Sources.”(后章会有详细说明)。

Global variables, shared memory, and objects |另外一种通信的方式就是使用全局变量、共享对象,或是共享内存块。虽然共享变量既快又简单,这种方式相比较直接消息方式更加脆弱。必须使用锁或者其他同步机制来“保护”共享变量。否则,可能导致线程间的竞争状态、数据被损坏,或程序崩溃。

Conditions|条件空之是控制线程同步的另一个工具。可以把条件控制看做是一个“门卫”,它只会在特定条件符合的时候才允许线程执行。更多信息,请查看“Using Conditions.”

Run loop sources |在线程中加入运行回路源可以让你接收到程序的特定消息。由于运行回路是“事件驱动”的,它会在无事可做的时候会自动让线程休眠,从而提高线程运行的效率。更多关于运行回路和运行回路源的信息,请查看“Run Loops.”

Ports and sockets |基于端口的通信是一种更加复杂的方式,但是它非常稳定。更重要的是,端口和套接字可以被在外部实体的访问,比如说其他的进程(process)和服务(service)。为了提高效率,端口使用运行回路源协助执行,因此线程在端口上无等待数据的时候会自动休眠。更多关于运行回路和基于端口的输入源(input source)方面的信息,请查看“Run Loops.”

Message queues| 多进程的服务中定义了一中先进先出(FIFO)队列的抽象模型来管理接收和传出的数据。虽然消息队列简单且方便,但它不像其他通信技术那样高效。关于如何使用消息队列,请查看Multiprocessing Services Programming Guide.

Cocoa distributed objects|分布式对象是Cocoa技术中的一项,它为基于端口通信的实现提供了一种高级的方式。虽然也可以把分布式对象用在线程间通信中,但是不建议这样做,因为会导致很高的系统开销。分布式对象在进程间通信中可以很好的排上用场,因为进程间通信本身就已经有较高的开销了。更多信息,请查看Distributed Objects Programming Topics.


线程设计建议
以下内容为多线程设计的指导,可以帮助你正确实现多线程代码。其中还包涵了一些提高线程性能的建议。和其他建议中提到的一样,在修改代码之前,修改代码时和修改后收集相关性能报表。

避免直接创建线程
手动编写创建线程的代码费时费力,且易出隐含错误,因此应尽可能避免这样做。Mac OS X和iOS通过其他API提供了对并行间接的支持。可以考虑一下使用异步的API, G~C~D或是操作对象来实现任务。这些技术可以帮助你完成线程相关的工作,你不需要考虑内部的复杂性,而且系统保证这些操作是正确的。另外,像G~C~D和operation object这些技术被设计用来更高效的管理线程,通过衡量当前的系统负载来调整活跃线程个数,其效率远远高于自己通过代码来实现。 更多关于G~C~D和operation objects的内容,请参考Concurrency Programming Guide

保持线程适度的忙状态
在手动创建、管理线程时,时刻谨记线程是会消耗宝贵的系统资源的。你要尽力保证线程的工作是长期的、有效的。同时,对于那些长时间出于空闲状态的线程,要毫不犹豫的处理掉。线程会占用大量的内存,所以释放空闲线程不只是减少你自己程序的内存消耗,也会为其他系统进程节省更多物理内存空间。

重要信息:在结束空闲线程之前,你应该对当前程序的性能做一个基本的记录。在修改之后,要确保改动是提高性能,而不是影响性能。

避免共享数据结构
避免线程相关资源冲突最简单直接的办法,就是给每个线程一份数据的拷贝。当你将线程之间的通信和资源竞争减到最小程度的时候,并行代码会工作的更好。

创建多线程的程序并不容易。即使你非常小心,在所有适当的地方锁住了共享数据,你的代码在语义上可能还是不安全的。例如,在你想要以某种书序修改一份共享数据时,就可能遇到问题。若为了解决这样的问题,使用基于事务的模型,又可能导致性能下降,而背离了使用多线程的初衷。在一开始就减少资源竞争,既可以简化设计,又能够获得好的性能。

线程和UI
如果你的程序拥有图形界面,建议在主线程中实现用户事件的接收,以及界面的刷新等动作。这样做可以避免绘制窗口内容和事件处理方面的同步问题。某些framework,例如Cocoa,通常会要求你这样做,但即便是没有要求,在主线程中做这些事情也会让你受益,它简化了管理UI的逻辑。

但是,有几个明显的反例。在这些例子里,在线程中执行绘图任务反而是有好处的。例如,QuickTime的API中包括了几个可以在子线程中执行的操作,如打开电影文件、绘制电影文件、压缩电影文件、导入和导出图片。同样,在Carbon和Cocoa中,你可以在线程内部创建、处理图片或做其他和图片相关的计算。这样可以大幅度提升性能。如果你不确定某个图像操作是否可以这样,还是老实的在主线程中执行吧。

更多关于QuickTime的线程安全知识,参考Technical Note TN2125:"Thread-Safe Programming in QuickTime".更多关于Cocoa线程安全性的信息,参考“Thread Safety Summary.” 更多关于Cocoa中绘制方面的信息,参考Cocoa Drawing Guide。

了解退出时线程的行为
当进程中所有非分离的(nondetached)线程退出后,进程结束。默认状态下,只有程序的主线程被创建成为这种。但是你也可以用这种方式创建子线程。当用户结束程序时,通常情况下,立即结束所有分离的(detached)线程是合适的。因为分离的线程完成的任务一般不是最重要的。但是,当你的程序使用后台线程来存储数据或是其他重要工作时,你可能需要创建非分离的线程,来保证在程序退出的时候不丢失数据。

创建非分离的(也被叫做“可会合的”)线程需要额外的工作。因为很多高层次的线程技术默认是不会按这种方式创建的。你可能需POSIX API来创建自己的线程。另外,你必须在主线程中编写代码,在程序退出时,执行子线程和主线程会合的操作。更多创建可会合的线程方面的信息,参考“Setting the Detached State of a Thread.”

如果你要编写Cocoa程序,你也可以用applicationShouldTerminate: 的delegate方法来延迟程序的退出,或者把线程操作一并取消(该句待校准)。当延迟退出时,你的程序需要等待重要线程的完成,在其完成后调用replyToApplicationShouldTerminate:方法,更多信息,请参考NSApplication Class Reference。

处理异常
异常处理机制基于当前的调用栈(call stack),在有异常被抛出时,执行必要的清除操作。由于每个线程都有独立的调用栈,因此需要各自负责捕获异常。在子线程捕获异常失败的后果和在主线程中一样:进程会终结。不能将未捕获的异常抛给另外的线程来处理。

如果你需要告知另外的线程(如主线程)当前线程中的异常状态,你应该先捕获该异常,然后发送一条消息告诉那个线程出问题了。根据你的设计和需要,捕获异常的线程可以继续执行(如果可能的话)、等待指令或是退出。

注意:在Cocoa中,NSException对象是一个独立、完整的对象,在捕获后,可以在线程间传递。

在某些情况下,异常处理机制常自动添加。例如,Objective-C中的@synchronized指令就包含了隐含的异常处理。

“干净”地解决掉线程
最好的方式是在到达程序主入口的终点时,自然结束掉线程。另外,我们还有方法来立即结束线程,除非不得以,不应使用那些方法。在线程“自然死”之前,强制结束它会影响线程的清理工作。如果线程申请了内存,打开过文件,或是请求过其他类型的资源,你的代码将无法回收那些资源,从而导致内存泄露或其他隐含的问题。

更多关于正确结束线程的信息,参考“Terminating a Thread.”

库中的线程安全性
程序开发者可以控制是否使用多线程,但库开发者就没有这个自由了。当开发一个库的时候,你必须假定调用库的程序使用了多线程或可以在任何时候切换到多线程模式。因此,必须一直使用锁来保护临界区代码。

对于库开发者来说,仅在程序转多线程模式时使用锁是不明智的。如果你需要在某个点上锁住代码操作,在使用库之前创建锁对象,最好调用隐式代码来初始化库(待校准。。。)。尽管你也可以使用静态库的初始化方法来创建这样的锁对象,但不到不得已,不要这样做。执行初始化方法会增加加载库的时间,从而明显影响性能。

注意:始终牢记在库中平衡锁和解锁的操作。你还应该注意锁住库中的数据,而不是靠调用代码来提供一个线程安全的环境。

如果你要开发Cocoa的库,你可以注册一个观察者对象(observer),针对键名为NSWillBecomeMultiThreadedNotification。这样在程序变为多线程方式时,你就能收到通知。但不能依赖于这个通知,因为它可能在你库中的代码被调用前被发送出来。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值