【IOS开发】《多线程编程指南》笔记
线程是单个应用中可以并发执行多个代码路径的多种技术之一。虽然更新的技术如操作对象(Operation)和Grand Central Dispatch(GCD),提供一个等价现代化和高效的基础设施来实现多核并发,但是Mac OS 和IOS也提供一套接口来创建和管理线程。
第一章:关于多线程编程
处理器已经达到瓶颈限制,所以芯片开始转向多核,这就是为什么要多核并发。
1.1 什么是多线程
多线程是一个比较轻量级的方法来实现单个应用程序多个代码执行路径。
在非并发程序中,只有一个执行程序,该线程开始和结束与你应用程序的main循环。一个个方法和函数的分支构成了你整个应用程序的所有行为。与此相反,支持并发的应用程序开始可以在需要额外的执行路径的时候创建一个或多个线程。每个新的执行路径有它自己独立于应用程序main循环的定制开始循环。在应用程序中存在多个线程提供了两个非常重要的潜在优势。
- 多个线程可以提高应用程序的感知响应。
- 多个线程可以提高应用程序在多核系统上的实时性能。
多线程的问题:因为多个线程这间可能有交互,会访问相同的数据结构。如果两个程序试图同时访问相同的数据结构,那么容易对数据进行交叉影响。
1.2 编程术语
线程(thread):用于指代独立执行的代码段。
进程(process):用于指代一个正在运行的可执行程序,他包含多个线程。
任务(task):用于指代抽象的概念,表示需要执行工作。
1.3 多线程替代方法
- operation objects
- Grand Central Dispatch
- Idle-time notifications
- Asynchronous functions
- Timers
- Separate processes
注意:当使用fork函数加载独立进程的时候,你必须总是在fork后面调用exec或者类似的函数。基于 Core Foundation、Cocao 或者 Core Data 框架(无论显式还是隐式关联)的应用程序随后调用 exec 函数或者类似的函数都会导出不确定的结果。
1.4 线程支持
1.4.1 线程包
多线程的底层实现机制是Mach的线程。但是很少使用Mach级的线程。我们经常使用的时POSIX的API或者它的衍生工具。
应用程序中使用的线程技术:
- Cocoa threads
- POSIX threads
- Multiprocessing Services
线程状态:运行(running)、就绪(ready)、阻塞(blocked)。
创建线程必须指定该线程的入口点函数(Cocoa线程为入口点方法),函数是由你想在该线程上执行的代码组成,但函数返回的时候,或你显式的中断线程的时候,线程永久停止,并且被系统回收。因为它的内存和时间消耗很大,所以在入口函数要做相当数量的工作,或建立一个运行循环允许进行经常性工作。
1.4.2 Run Loops
Run Loop是用来在线程上管理事件异步到达的基础设施,一个run loop为线程检测一个或多个事件源。
1.4.3 同步工具
线程编程的危害就是多个资源争夺,所以我们必须尽可能的使用锁,条件,原子操作和其他技术来同步资源的访问。
锁:
提供了一次只有一个线程可以执行代码的有效保护形式。最简单的一种锁互斥排他锁(mutex)。
Cocoa提供了几个互斥排他锁的变种来支持不同的行为类型,比如递归。
条件:
确保应用程序任务执行的适当顺序,一个条件作为一个看门人,阻塞给定的线程。POSIZ级别和基础框架都直接提供了条件的支持。(如果你使用操作对象,你可以配置你的操作对象之间的依赖关系的顺序确 定任务的执行顺序,这和条件提供的行为非常相似)。
原子操作:
可以执行标量数据类型的数学或逻辑运算,原子操作使用特殊的硬件设施来保证变量的改变在其他线程可以访问前完成。
1.4.4 线程间通信
Mac OS X上面使用的通信机制。(异常的消息队列和Cocoa分布式对象,这些也可以在IOS上通信)
- Direct messaging
- Global variables, shared memory,and objects
- Conditions
- Run loop sources
- Ports and sockets
- Message queues
- Cocoa distributed objects
Mac OS X 和 iOS 通过其他 API 接口提供了隐式的并发支持。你可以考虑使用异步 API, GCD 方式,或操作对象来实现并发,而不是自己创建一个线程。这些技术背后为你做 了线程相关的工作,并保证是无误的。 此外,比如 GCD 和操作对象技术被设计用来管理线程,比通过自己的代码根据当前的负载调整活动线程的数量更高效。
1.5.2 保持你的线程合理的忙
1.5.3 避免共享数据结构
最简单的方法就是给你应用程序的每个线程一份它需求的数据的副本。
1.5.4 多线程和你的用户界面
如果程序有图形界面,建议在主线程中接受和界面相关的时间和初始化更新你的界面。有利于避免与处理用户事件和窗口绘图相关的同步问题。
1.5.5 了解线程退出时的行为
如果你正在编程 Cocoa 的程序,你也可以通过使用 applicationShouldTerminate: 的委托方法来延迟程序的中断直到一段时间后或者完成取消。当延迟中断的时候,你 的程序需要等待直到任何周期线程已经完成它们的任务且调用了 replyToApplicationShouldTerminate:方法。
1.5.6 异常处理
如果你需要通知另一个线程(比如主线程)当前线程中的一个特殊情况,你应该 捕捉异常,并简单地将消息发送到其他线程告知发生了什么事。根据你的模型和你正 在尝试做的事情,引发异常的线程可以继续执行(如果可能的话),等待指示,或者 干脆退出。
注意:在 Cocoa 里面,一个 NSException 对象是一个自包含对象,一旦它被引发了,那么它 可以从一个线程传递到另外一个线程。
在一些情况下,异常处理可能是自动创建的。比如,Objective-C 中的 @synchronized 包含了一个隐式的异常处理。
1.5.7 干净的中端你的线程
第二章:线程管理
2.1 线程成本
2.2 创建一个线程
2.2.1 使用NSThread
使用NSThread来创建线程有两个方法
使用 detachNewThreadSelector:toTarget:withObject:类方法来生成一个
新的线程。
创建一个新的 NSThread 对象,并调用它的 start 方法。(仅在 iOS 和 Mac OS X v10.5 及其之后才支持)
initWithTarget:selector:object:方法。该方法和 detachNewThreadSelector:toTarget:withObject:方法来初始化一个新的 NSThread 实例需要相同的额外开销。然而它并没有启动一个线程。为了启动一个线程,你可以 显式调用先对象的 start 方法
2.2.2 使用POSIX的多线程
2.2.3 使用NSObject来生成一个线程
在 iOS 和 Mac OS X v10.5 及其之后,所有的对象都可能生成一个新的线程,并 用它来执行它任意的方法。方法 performSelectorInBackground:withObject:新生成一个脱离的线程,使用指定的方法作为新线程的主体入口点。
比如,如果你有一些对 象(使用变量 myObj 来代表),并且这些对象拥有一个你想在后台运行的 doSomething 的方法,你可以使用如下的代码来生成一个新的线程:
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
调用该方法的效果和你在当前对象里面使用 NSThread 的 detachNewThreadSelector:toTarget:withObject:传递 selectore,object 作为参数的方法一样。
2.2.4 使用其他线程技术
尽管 POSIX 例程和 NSThread 类被推荐使用来创建低级线程,但是其他基于 C 语 言的技术在 Mac OS X 上面同样可用。在这其中,唯一一个可以考虑使用的是多处理 服务(Multiprocessing Services),它本身就是在 POSIX 线程上执行。多处理服务 是专门为早期的 Mac OS 版本开发的,后来在 Mac OS X 里面的 Carbon 应用程序上面 同样适用。如果你有代码真是有该技术,你可以继续使用它,尽管你应该把这些代码 转化为 POSIX。该技术在 iOS 上面不可用。
2.2.5 在cocoa程序中使用POSIX线程
2.3 配置线程属性
2.3.1 配置线程的堆栈大小
Technology |
Option |
||||
Cocoa |
In iOS and Mac OS X v10.5 and later, allocate and initialize an NSThread object (do not use thedetachNewThreadSelector:toTarget:withObject: method). Before calling the start method of the thread object, use thesetStackSize: method to specify the new stack size. |
||||
POSIX |
Create a new pthread_attr_t structure and use the pthread_attr_setstacksize function to change the default stack size. Pass the attributes to the pthread_create function when creating your thread. |
||||
Multiprocessing Services |
Pass the appropriate stack size value to the MPCreateTask function when you create your thread. |
2.3.2 配置线程本地存储
每个线程都维护了一个键—值的字典,他可以在线程里面的任何地方被访问。你可以使用该字典保存一些信息。
2.3.3 设置线程的脱离状态
大部分上层的线程技术都默认创建了脱离线程(Detached thread),他们允许系统在线程完成的时候立即释放他的数据结构。脱离线程不需要显示和你的应用程序交互。意味着线程检索的结果由你来决定。相比之下,系统不回收可连接线程(joinable thread)的资源,知道另一个线程明确加入该线程,这个过程中可能会阻止线程执行加入。
可以认为可连接线程类似于子线程,虽然可以作为独立线程运行,但是可连接线程在它资源可以呗系统回收之前必须被其他线程连接,可连接线程提供一个显示的方式把数据传递到其他线程。可以传递一个数据指针或者返回值给pthread_exit函数。其他函数可以通过pthread_join拿到这个数据。
重要:在应用程序退出时,脱离线程可以立即被中断,而可连接线程则不可以。每个可连接 线程必须在进程被允许可以退出的时候被连接。所以当线程处于周期性工作而不允许被中断的时 候,比如保存数据到硬盘,可连接线程是最佳选择。
如果你想要创建可连接线程,唯一的办法是使用 POSIX 线程。POSIX 默认创建的 线程是可连接的。为了把线程标记为脱离的或可连接的,使用 pthread_attr_setdetachstate 函数来修改正在创建的线程的属性。在线程启动后, 你可以通过调用 pthread_detach 函数来把线程修改为可连接的。
2.3.4 设置线程的优先级
如果你想改变线程的优先级,Cocoa 和 POSIX 都提供了一种方法来实现。对于 Cocoa 线程而言,你可以使用 NSThread 的 setThreadPriority:类方法来设置当前运 行线程的优先级。对于 POSIX 线程,你可以使用 pthread_setschedparam 函数来实现。
2.4 编写你线程的主题入口点
2.4.1 创建一个自动释放池
在OC框架链接的应用程序,通常每一个线程必须创建至少一个自动释放池。
如果你的应用使用内存管理模型,在你编写线程主体入口的时候第一件事情就是创建一个自动释放池,同样,在线程的最后应该销毁该自动释放池。
2.4.2 设置异常处理
在异常发生的地方捕捉并且处理它,但是如果在你的线程里面捕捉一个抛出的异常失败的话可能照成你的应用程序强退,在你线程的主要入口点安装一个try/catch模块,可以捕捉任何未知的异常,并提供一个合适的响应。
2.4.3 设置一个run loop
当你想编写一个独立运行的线程时,你有两个选择。第一种选择是写代码作为一个长期任务,很少甚至不中断,线程完成的时候退出。第二种是把线程放在一个循环中,让他动态的处理到来的任务请求。
cocoa,carbon,UIKit在你的应用程序的主线程中自动启动了一个run loop,但是如果你要创建人和网辅助线程,你必须自己手工设置一个run lloop并且启动他。
2.5 中断线程
退出一个线程推荐的方法是让它在它主体入口点正常退出,尽管Cocoa、POSIX和Mutiprocessing Services提供了直接杀死线程的方法。但是使用这些方法是强烈不鼓励的,因为他们阻止了线程本身清理工作。可能照成内存泄露,并且其他线程当前使用的资源可能没有被正确清理干净,之后照成潜在的问题。
如果程序需要中断一个线程,应该设计线程响应取消或退出的消息。
响应取消消息的一个方法是使用 run loop 的输入源来接收这些消息。列表 2-3 显示了该结构的类似代码在你的线程的主体入口里面是怎么样的(该示例显示了主循 环部分,不包括设立一个自动释放池或配置实际的工作步骤)。该示例在 run loop 上面安装了一个自定义的输入源,它可以从其他线程接收消息。关于更多设置输入源 的信息,参阅“配置 Run Loop 源”。执行工作的总和的一部分后,线程运行的 run loop 来查看是否有消息抵达输入源。如果没有,run loop 立即退出,并且循环继续处理 下一个数据块。因为该处理器并没有直接的访问 exitNow 局部变量,退出条件是通过 线程的字典来传输的。
Listing 2-3 Checking for an exit condition during a long job
- (void)threadMainRoutine { BOOL moreWorkToDo = YES; BOOL exitNow = NO; NSRunLoop* runLoop = [NSRunLoop currentRunLoop]; // Add the exitNow BOOL to the thread dictionary. NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; [threadDict setValue:[NSNumber numberWithBool:exitNow]forKey:@"ThreadShouldExitNow"];  // Install an input source.  [self myInstallCustomInputSource];  while (moreWorkToDo && !exitNow)  {  // Do one chunk of a larger body of work here.// Change the value of the moreWorkToDo Boolean when done.// Run the run loop but timeout immediately if the input source isn't waiting to fire. [runLoop runUntilDate:[NSDate date]]; // Check to see if an input source handler changed the exitNow value. exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue]; } 
第三章 run loop
Run loop 接收输入事件来自两种不同的来源:输入源(input source)和定时源 (timer source)。输入源传递异步事件,通常消息来自于其他线程或程序。定时源 则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特 定的处理例程来处理到达的事件。