什么是线程?
一个CPU执行的CPU命令列是一条无分叉的路径,这个路径就是线程。
如果在同一个进程(process)中能够实现多条代码执行路径,那么这种系统就具备了多线程能力了。可以注意的是,是系统具备能力,而不是自己写的呆萌的程序自己搞出来的。所以线程概念应该用系统级思路去理解,应用的时候在落实到应用代码实现。
进程有自己的存储空间,但是进程内的线程们都有自己的堆栈空间。这就涉及到一个牛逼哄哄的概念,叫“上下文切换”。人们分析性能啊,多核心切换啊,总是用这东西。其实cpu在执行指令的时候会先把内存中所需要处理的变量拿到寄存器里面去,如果有多个执行路径,cpu就得切换一下进程对应的堆栈空间,也就是内存块,然后执行操作。保存的时候也要切换,就是这么个概念。而这个工作在Mac OS和iOS中是由XNU内核搞定的。
多线程编程实践的时候可能会遇到一些问题:
- 数据竞争
thread A 改一个数据,thread B也改,没有版本维护的前提下,就改乱了。乱,被叫做数据不一致。
人们会有很多办法来解决这个事情,这些办法有个统称,叫线程同步。
比如你有个变量存储了剩余票数,threadA卖 ,同时ThreadB也卖,但是当A判断票数==1的时候,切换到了B去执行,回到A的时候票数==0,但是A已经判断过票数>0,继续卖票。
- 线程死锁
thread A继续执行需要等待threadB的一个操作结束,tread B也在等A的一个操作,互相等的过程。
- 性能问题
XNU切换路径费时间,线程本身要分配进程内存空间。忍的了还是忍不了
真tm麻烦,麻烦,那还用不用。用!因为多线程编程会带来一个很好的体验,就是使得应用程序的响应性能提高了。
在iOS中,主线程有个RunLoop,这货的作用是对事件的派发处理,你比如,你点击个按钮,按钮会变个色,表示自己被点击了,这个重绘过程是在主线程执行的。如果主线程里有个耗时的读数据操作,主线程被阻塞,runLoop也就不会发送你的点击事件了,好像这个程序不动了。这就是无法响应,还有个专业的称呼叫挂起。
说了这么多,我们从技术角度来描述一下线程:
一个线程就是一个需要管理执行代码的内核级结构和应用级数据结构组合。内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。应用级结构包括用于存储函数调用的调用堆栈和应用程序需要管理和操作线程属性和状态的结构。
好,我要创建线程啦
等一等!我们听一听前辈的忠告:
你自己创建多线程代码的一个问题就是它会给你的代码带来不确定性。多线程是一个相对较低的水平和复杂的方式来支持你的应用程序并发。如果你不完全理解你的设计选择的影响,你可能很容易遇到同步或定时问题,其范围可以从细微的行为变化到严重到让你的应用程序崩溃并破坏用户数据。
你需要考虑的另一个因素是你是否真的需要多线程或并发。多线程解决了如何在同一个进程内并发的执行多路代码路径的问题。然而在很多情况下你是无法保证你所在做的工作是并发的。多线程引入带来大量的开销,包括内存消耗和CPU占用。你会发现这些开销对于你的工作而言实在太大,或者有其他方法会更容易实现。
一盆子凉水就凉透了,输出不爆炸,顺风不能浪,宝宝心理苦,宝宝不说。
但是,iOS中有两种常用替代方案:
- Operation objects
- Grand Central Dispatch (GCD)
这些方案可以在编程实践中以更高的抽象程度,更加友好的API实现应用程序并发,请使用,并常使用。
不幸的是,你已经使用了底层的多线程实现
Mac OS X和iOS提供几种技术来在你的应用程序里面创建多线程。此外,两个系统都提供了管理和同步你需要在这些线程里面处理的工作。所以,忍着幸苦再学学,但是可能用处不大,到处是坑。
技术 | 描述 |
---|---|
Cocoa thread | 实现这种方式需要用到NSThread类。Cocoa也给NSObject提供许多方法来spawn新的线程或者在已经存在的线程上执行代码。 |
POSIX threads | 它提供一个基于C的借口来创建线程。如果编写的不是一个Cocoa应用的话,这回事一个很好的选择。POSIX接口是相对简单的。为使用和配置你的线程提供了充分的灵活性。 |
Multiprocessing Services | 这是个传统的基于C的接口。应用程序可以使用它实现从老版本Mac OS的过渡。只在Mac OS X中可以被采用。 |
在应用层上,其他平台一样所有线程的行为本质上是相同的。线程启动之后,线程就进入三个状态中的任何一个:运行(running)、就绪(ready)、阻塞(blocked)。如果一个线程当前没有运行,那么它不是处于阻塞,就是等待外部输入,或者已经准备就绪等待分配CPU。线程持续在这三个状态之间切换,直到它最终退出或者进入中断状态。
我好像都明白了,并写了一个库
上面的描述可能比较浅显,但是好歹算是个提示。当你使用了底层API或者NSThread显示的创建了线程并且使用在应用程序中,有些小tip提供给你:
- 学习一下RunLoop
一个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”部分介绍。
- 同步工具
线程编程的危害之一是在多个线程之间的资源争夺。如果多个线程在同一个时间试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源,并确保每个线程都有在它操作的资源上面的独特设置。因为保持完全独立的资源是不可行的,所以你可能必须使用锁,条件,原子操作和其他技术来同步资源的访问。
锁提供了一次只有一个线程可以执行代码的有效保护形式。最普遍的一种锁是互斥排他锁,也就是我们通常所说的“mutex”。当一个线程试图获取一个当前已经被其他线程占据的互斥锁的时候,它就会被阻塞直到其他线程释放该互斥锁。系统的几个框架提供了对互斥锁的支持,虽然它们都是基于相同的底层技术。此外Cocoa提供了几个互斥锁的变种来支持不同的行为类型,比如递归。获取更多关于锁的种类的信息,请阅读“锁”部分内容。
除了锁,系统还提供了条件,确保在你的应用程序任务执行的适当顺序。一个条件作为一个看门人,阻塞给定的线程,直到它代表的条件变为真。当发生这种情况的时候,条件释放该线程并允许它继续执行。POSIX级别和基础框架都直接提供了条件的支持。(如果你使用操作对象,你可以配置你的操作对象之间的依赖关系的顺序确定任务的执行顺序,这和条件提供的行为非常相似)。
尽管锁和条件在并发设计中使用非常普遍,原子操作也是另外一种保护和同步访问数据的方法。原子操作在以下情况的时候提供了替代锁的轻量级的方法,其中你可以执行标量数据类型的数学或逻辑运算。原子操作使用特殊的硬件设施来保证变量的改变在其他线程可以访问之前完成。
- 进程间通信
机制 | 描述 |
---|---|
Direct Message | Cocoa应用支持在线程中执行selector。这个能力使得开发者可以让一个线程在另个线程中执行方法。此时执行的上下文是目标线程,通过这种方式发送的消息会自动序列化到目标线程上。 |
Global variables,shared memory,and objects | 另外一种简单的通信方式是使用全局变量,共享对象或者使用一块共享内存。尽管,共享变量这种方式,简单且快捷。但是却比Direct Message这种方式更加脆弱。共享的变量必须小心被锁或者其它线程同步技术保护起来。否则可能会引起资源竞争,数据损坏,程序奔溃等。 |
Conditions | Conditions是线程同步的一个工具。使用它可以控制线程执行一段特定的代码。 |
Run loop sources | 一个自定义的runloop,这样你在线程中就可以接收到系统特定的消息。runloop是事件驱动的,没有事情做的时候,runloop会将线程设置成休眠状态。这会使得你实现的线程更加高效。 |
Ports and sockets | 基于端口的通信是极为精细的一种线程通信方式。它还能和其它进程和服务通信。这种方式一般也是通过Run loop sources实现。 |
Cocoa distributed objects | 是个抽象的port-basedde 技术。更多是核心线程间通信,多用在不同进程间。 |
这下没什么了,要开干了。
在开干之前还有一些主意和技巧,希望能列出来参考一下。
- 避免显式创建线程
手动编写线程创建代码是乏味的,而且容易出现错误,你应该尽可能避免这样做。Mac OS X和iOS通过其他API接口提供了隐式的并发支持。你可以考虑使用异步API,GCD方式,或操作对象来实现并发,而不是自己创建一个线程。这些技术背后为你做了线程相关的工作,并保证是无误的。此外,比如GCD和操作对象技术被设计用来管理线程,比通过自己的代码根据当前的负载调整活动线程的数量更高效。 关于更多GCD和操作对象的信息,你可以查阅“并发编程指南(Concurrency Programming Guid)”。
- 保持你的线程合理的忙
如果你准备人工创建和管理线程,记得多线程消耗系统宝贵的资源。你应该尽最大努力确保任何你分配到线程的任务是运行相当长时间和富有成效的。同时你不应该害怕中断那些消耗最大空闲时间的线程。线程使用一个平凡的内存量,它的一些有线,所以释放一个空闲线程,不仅有助于降低您的应用程序的内存占用,它也释放出更多的物理内存使用的其他系统进程。线程占用一定量的内存,其中一些是有线的,所以释放空闲线程不但帮助你减少了你应用程序的内存印记,而且还能释放出更多的物理内存给其他系统进程使用。
- 避免共享数据结构
避免造成线程相关资源冲突的最简单最容易的办法是给你应用程序的每个线程一份它需求的数据的副本。当最小化线程之间的通信和资源争夺时并行代码的效果最好。
创建多线程的应用是很困难的。即使你非常小心,并且在你的代码里面所有正确的地方锁住共享资源,你的代码依然可能语义不安全的。比如,当在一个特定的顺序里面修改共享数据结构的时候,你的代码有可能遇到问题。以原子方式修改你的代码,来弥补可能随后对多线程性能产生损耗的情况。把避免资源争夺放在首位通常可以得到简单的设计同样具有高性能的效果。
- 多线程和你的用户界面
如果你的应用程序具有一个图形用户界面,建议你在主线程里面接收和界面相关的事件和初始化更新你的界面。这种方法有助于避免与处理用户事件和窗口绘图相关的同步问题。一些框架,比如Cocoa,通常需要这样操作,但是它的事件处理可以不这样做,在主线程上保持这种行为的优势在于简化了管理你应用程序用户界面的逻辑。
有几个显著的例外,它有利于在其他线程执行图形操作。比如,QuickTime API包含了一系列可以在辅助线程执行的操作,包括打开视频文件,渲染视频文件,压缩视频文件,和导入导出图像。类似的,在Carbon和Cocoa里面,你可以使用辅助线程来创建和处理图片和其他图片相关的计算。使用辅助线程来执行这些操作可以极大提高性能。如果你不确定一个操作是否和图像处理相关,那么你应该在主线程执行这些操作。
关于QuickTime线程安全的信息,查阅Technical Note TN2125:“QuickTime的线程安全编程”。关于Cocoa线程安全的更多信息,查阅“线程安全总结”。关于Cocoa绘画信息,查阅Cocoa绘画指南(Cocoa Drawing Guide)。
- 了解线程退出时的行为
进程一直运行直到所有非独立线程都已经退出为止。默认情况下,只有应用程序的主线程是以非独立的方式创建的,但是你也可以使用同样的方法来创建其他线程。当用户退出程序的时候,通常考虑适当的立即中断所有独立线程,因为通常独立线程所做的工作都是是可选的。如果你的应用程序使用后台线程来保存数据到硬盘或者做其他周期行的工作,那么你可能想把这些线程创建为非独立的来保证程序退出的时候不丢失数据。
以非独立的方式创建线程(又称作为可连接的)你需要做一些额外的工作。因为大部分上层线程封装技术默认情况下并没有提供创建可连接的线程,你必须使用POSIX API来创建你想要的线程。此外,你必须在你的主线程添加代码,来当它们最终退出的时候连接非独立的线程。更多有关创建可连接的线程信息,请查阅“设置线程的脱离状态”部分。
如果你正在编程Cocoa的程序,你也可以通过使用applicationShouldTerminate:的委托方法来延迟程序的中断直到一段时间后或者完成取消。当延迟中断的时候,你的程序需要等待直到任何周期线程已经完成它们的任务且调用了replyToApplicationShouldTerminate:方法。关于更多这些方法的信息,请查阅NSApplication Class Reference。
- 处理异常
当抛出一个异常时,异常的处理机制依赖于当前调用堆栈执行任何必要的清理。因为每个线程都有它自己的调用堆栈,所以每个线程都负责捕获它自己的异常。如果在辅助线程里面捕获一个抛出的异常失败,那么你的主线程也同样捕获该异常失败:它所属的进程就会中断。你无法捕获同一个进程里面其他线程抛出的异常。
如果你需要通知另一个线程(比如主线程)当前线程中的一个特殊情况,你应该捕捉异常,并简单地将消息发送到其他线程告知发生了什么事。根据你的模型和你正在尝试做的事情,引发异常的线程可以继续执行(如果可能的话),等待指示,或者干脆退出。
注意:在Cocoa里面,一个NSException对象是一个自包含对象,一旦它被引发了,那么它可以从一个线程传递到另外一个线程。
- 干净地中断你的线程
线程自然退出的最好方式是让它达到其主入口结束点。虽然有不少函数可以用来立即中断线程,但是这些函数应仅用于作为最后的手段。在线程达到它自然结束点之前中断一个线程阻碍该线程清理完成它自己。如果线程已经分配了内存,打开了文件,或者获取了其他类型资源,你的代码可能没办法回收这些资源,结果造成内存泄漏或者其他潜在的问题。
- 线程安全的库
虽然应用程序开发人员控制应用程序是否执行多个线程,类库的开发者则无法这样控制。当开发类库时,你必须假设调用应用程序是多线程,或者多线程之间可以随时切换。因此你应该总是在你的临界区使用锁功能。
对类库开发者而言,只当应用程序是多线程的时候才创建锁是不明智的。如果你需要锁定你代码中的某些部分,早期应该创建锁对象给你的类库使用,更好是显式调用初始化类库。虽然你也可以使用静态库的初始化函数来创建这些锁,但是仅当没有其他方式的才应该这样做。执行初始化函数需要延长加载你类库的时间,且可能对你程序性能造成不利影响。