参考书籍 《多线程编程指南》
原著:Apple Inc.
翻译:謝業蘭 【老狼】
目录
什么是多线程
多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径。
从技术角度来看,一个线程 = 应用级结构 + 内核级结构。
1.应用级结构:存储函数调用堆栈以及管理线程的属性和状态的结构。
2.内核级结构:协助调度线程事件,并抢占式调度一个线程到可用的内核之上。调度点由系统决定。
如果没有内核级结构,在某一个进程中当用户的一个线程进入内核,内核需要中断时(比如打印)就没法去执行另外一个线程而是直接切换到了另外一个进程执行
多线程的优点:
-
多个线程可以提高应用程序的感知响应。
比如把你的计算任务转移到一个独立的线程里面,那么应用程序的主线程就可以自由并及时响应用户的交互。而不是让用户等待很长时间而误认为程序已经被挂起。 -
多个线程可以提高应用程序在多核系统上的实时性能。
多线程的缺点
-
增加了代码的复杂性
应用程序内拥有多个线程时,每个线程需要和其他线程协调其行为。 -
破坏数据
由于多个线程共享内存空间并访问相同的数据结构,会导致数据被破坏。需要手段进行保护。 -
多线程引入带来大量的开销,包括内存消耗和CPU占用
线程术语
进程(process): 用于指代一个正在运行的可执行程序,它可以包含多个线程。
线程(thread) : 用于指代独立执行的代码段。
任务(task) : 用于指代抽象的概念,表示需要执行工作。
多线程的替代方法
多线程相对偏向于底层,如果不完全理解可能很容易遇到同步或定时问题。
另一个因素是是否真的需要多线程或并发。在很多情况下你是无法保证你所做的工作是并发的。多线程引入带来大量的开销,包括内存消耗和CPU占用。你会发现这些开销对于你的工作而言实在太大。
或者有其他方法会更容易实现
Operation
GCD
线程支持
一、线程包
启动 运行 就绪 阻塞 停止
线程启动之后,线程就进入三个状态中的任何一个:运行(running)、就绪(ready)、阻塞(blocked)。
线程持续在这三个状态之间切换,直到它最终退出或者进入中断状态。
当你创建一个新的线程,你必须指定该线程的入口点函数(或 Cocoa 线程时候为入口点方法)。该入口点函数由你想要在该线程上面执行的代码组成。但函数返回的时候,或你显式的中断线程的时候,线程永久停止,且被系统回收。因为线程创建需要的内存和时间消耗都比较大,因此建议:
你的入口点函数做相当数量的工作
或者
建立一个运行循环允许进行经常性的工作。
二、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。
三、同步工具
线程编程的危害之一是在多个线程之间的资源争夺。如果多个线程在同一个时间 试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法:
锁:同一代码,同一时间,只能被一个线程执行。
条件:一个条 件作为一个看门人,阻塞给定的线程,直到它代表的条件变为真
原子操作:替代锁的轻量级的方法,原子操作使用特殊的硬件设施来保证变量的 改变在其他线程可以访问之前完成。
四、线程间通信
虽然一个良好的设计最大限度地减少所需的通信量,但在某些时候,线程之间的 通信显得十分必要。(线程的任务是为你的应用程序工作,但如果从来没有使用过这 些工作的结果,那有什么好处呢?)线程可能需要处理新的工作要求,或向你应用程 序的主线程报告其进度情况。在这些情况下,你需要一个方式来从其他线程获取信息。 幸运的是,线程共享相同的进程空间,意味着你可以有大量的可选项来进行通信。
设计技巧
一、避免显式创建线程
手动编写线程创建代码是乏味的,而且容易出现错误,你应该尽可能避免这样做。应该更多的使用:
Operation
GCD
这些技术背后为你做 了线程相关的工作,并保证是无误的。此外,比如 GCD 和操作对象技术被设计用来管理线程,比通过自己的代码根据当前的负载调整活动线程的数量更高效。
二、 保持你的线程合理的忙
你应该尽最 大努力确保任何你分配到线程的任务是运行相当长时间和富有成效的。同时应该中断那些消耗最大且空闲的线程。
三、避免共享数据结构
把避免资源争夺放在首位通常可以得到简单的设计和高性能的效果。
锁
条件
原子操作
四、多线程和你的用户界面
如果你的应用程序具有一个图形用户界面,建议你在主线程里面接收和界面相关的事件和初始化更新你的界面。这种方法有助于避免与处理用户事件和窗口绘图相关的同步问题。一些框架,比如 Cocoa,通常需要这样操作,但是它的事件处理可以不这样做,在主线程上保持这种行为的优势在于简化了管理你应用程序用户界面的逻辑。
有几个显著的例外,它有利于在其他线程执行图形操作。比如,QuickTime API 包含了一系列可以在辅助线程执行的操作,包括打开视频文件,渲染视频文件,压缩 视频文件,和导入导出图像。类似的,在 Carbon 和 Cocoa 里面,你可以使用辅助线 程来创建和处理图片和其他图片相关的计算。使用辅助线程来执行这些操作可以极大 提高性能。如果你不确定一个操作是否和图像处理相关,那么你应该在主线程执行这 些操作。
五、了解线程退出时的行为
进程一直运行直到所有非独立线程都已经退出为止。默认情况下,只有应用程序的主线程是以非独立的方式创建的,但是你也可以使用同样的方法来创建其他线程。
当用户退出程序的时候,通常考虑适当的立即中断所有独立线程,因为通常独立线程所做的工作都是是可选的。如果你的应用程序使用后台线程来保存数据到硬盘或者做其他周期性的工作,那么你可能想把这些线程创建为非独立的来保证程序退出的时候 不丢失数据。
六、处理异常
每个线程都有它自己的调用堆栈,所以每个线程都负责捕获它自己的异常。
如果你需要通知另一个线程(比如主线程)当前线程中的一个特殊情况,你应该 捕捉异常,并简单地将消息发送到其他线程告知发生了什么事。
注意:在 Cocoa 里面,一个 NSException 对象是一个自包含对象,一旦它被引发了,那么它 可以从一个线程传递到另外一个线程。
七、干净地中断你的线程 线程自然退出的最好方式是让它达到其主入口结束点。虽然有不少函数可以用来 立即中断线程,但是这些函数应仅用于作为最后的手段。在线程达到它自然结束点之 前中断一个线程阻碍该线程清理完成它自己。如果线程已经分配了内存,打开了文件, 或者获取了其他类型资源,你的代码可能没办法回收这些资源,结果造成内存泄漏或 者其他潜在的问题。
八、线程安全的库
当开发类库时,你必须假设调用应用程序是多线程,或者多线程之间可以随 时切换。因此你应该总是在你的临界区使用锁功能。
注意:永远记住在你的类库里面保持锁和释放锁的操作平衡。你应该总是记住锁定类库的数 据结构,而不是依赖调用的代码提供线程安全环境。
如果你真正开发 Cocoa 的类库,那么当你想在应用程序变成多线程的时候收到通 知的话,你可以给 NSWillBecomeMultiThreadedNotification 注册一个观察者。不 过你不应用依赖于这些收到的通知,因为它们可能在你的类库被调用之前已经被发出 了。
行者常至,为者常成!