多线程编程指南
简介
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.
转载自:http://www.zeasy.cn/multithreaded-programming-guide/