iOS多线程编程指南

时间:2023-03-08 22:26:51

iOS多线程编程指南(拓展篇)(1)

一、Cocoa

在Cocoa上面使用多线程的指南包括以下这些:

(1)不可改变的对象一般是线程安全的。一旦你创建了它们,你可以把这些对象在线程间安全的传递。另一方面,可变对象通常不是线程安全的。为了在多线程应用里面使用可变对象,应用必须适当的同步。关于更多信息,参阅”可变和不可变对比”。

(2)许多对象在多线程里面不安全的使用被视为是”线程不安全的”。只要同一时间只有一个线程,那么许多这些对象可以被多个线程使用。这种被称为专门限制应用程序的主线程的对象通常被这样调用。

(3)应用的主线程负责处理事件。尽管Application Kit在其他线程被包含在事件路径里面时还会继续工作,但操作可能会被打乱顺序。

(4)如果你想使用一个线程来绘画一个视图,把所有绘画的代码放在NSView的lockFocusIfCanDraw和unlockFocus方法中间。

为了在Cocoa里面使用POSIX线程,你必须首先把Cocoa变为多线程模式。关于更多信息,参阅“在Cocoa应用里面使用POSIX线程”部分。

基础框架(Fondation Framework)的线程安全

有一种误解,认为基础框架(Foundation framework)是线程安全的,而Application Kit是非线程安全的。不幸的是,这是一个总的概括,从而造成一点误导。每个框架都包含了线程安全部分和非线程安全部分。以下部分介绍Foundation framework里面的线程安全部分。

线程安全的类和函数

下面这些类和函数通常被认为是线程安全的。你可以在多个线程里面使用它们的同一个实例,而无需获取一个锁。

非线程安全类

以下这些类和函数通常被认为是非线程安全的。在大部分情况下,你可以在任何线程里面使用这些类,只要你在同一个时间只在一个线程里面使用它们。参考这些类对于的额外详细信息的文档。

注意,尽管NSSerializer,NSArchiver,NSCoder和NSEnumerator对象本身是线程安全的,但是它们被放置这这里是因为当它们封装的对象被使用的时候,更改这些对象数据是不安全的。比如,在归档情况下,修改被归档的对象是不安全的。对于一个枚举,任何线程修改枚举的集合都是不安全的。

只能用于主线程的类

以下的类必须只能在应用的主线程类使用。

  • NSAppleScript

可变 vs 不可变

不可变对象通常是线程安全的。一旦你创建了它们,你可以把它们安全的在线程间传递。当前,在使用不可变对象时,你还应该记得正确使用引用计数。如果不适当的释放了一个你没有引用的对象,你在随后有可能造成一个异常。

可变对象通常是非线程安全的。为了在多线程应用里面使用可变对象,应用应该使用锁来同步访问它们(关于更多信息,参见“原子操作”部分)。通常情况下,集合类(比如,NSMutableArray,NSMutableDictionary)是考虑多变时是非线程安全的。这意味着,如果一个或多个线程同时改变一个数组,将会发生问题。你应该在线程读取和写入它们的地方使用锁包围着。

即使一个方法要求返回一个不可变对象,你不应该简单的假设返回的对象就是不可变的。依赖于方法的实现,返回的对象有可能是可变的或着不可变的。比如,一个返回类型是NSString的方法有可能实际上由于它的实现返回了一个NSMutableString。如果你想要确保对象是不可变的,你应该使用不可变的拷贝。

可重入性

可重入性是可以让同一对象或者不同对象上一个操作“调用”其他操作成为可能。保持和释放对象就是一个有可能被忽视的”调用”的例子。

以下列表列出了Foundation framework的部分显式的可重入对象。所有其他类可能是或可能不是可重入的,或者它们将来有可能是可重入的。对于可重入性的一个完整的分析是不可能完成的,而且该列表将会是无穷尽的。

类的初始化

Objective-C的运行时系统在类收到其他任何消息之前给它发送一个initialize消息。这可以让类有机会在它被使用前设置它的运行时环境。在一个多线程应用里面,运行时保证仅有一个线程(该线程恰好发送第一条消息给类)执行initialized方法,第二个线程阻塞直到第一个线程的initialize方法执行完成。在此期间,第一个线程可以继续调用其他类上的方法。该initialize方法不应该依赖于第二个线程对这个类的调用。如果不是这样的话,两个线程将会造成死锁。

自动释放池(Autorelease Pools)

每个线程都维护它自己的NSAutoreleasePool的栈对象。Cocoa希望在每个当前线程的栈里面有一个可用的自动释放池。如果一个自动释放池不可用,对象将不会给释放,从而造成内存泄露。对于Application Kit的主线程通常它会自动创建并消耗一个自动释放池,但是辅助线程(和其他只有Foundationd的程序)在使用Cocoa前必须自己手工创建。如果你的线程是长时间运行的,那么有可能潜在产生很多自动释放的对象,你应该周期性的销毁它们并创建自动释放池(就像Application Kit对主线程那样)。否则,自动释放对象将会积累并造成内存大量占用。如果你的脱离线程没有使用Cocoa,你不需要创建一个自动释放池。

Run Loops

每个线程都有一个或多个run loop。然而每个run loop和每个线程都有它自己的输入模式来决定run loop运行的释放监听那些输入源。输入模式定义在一个run loop上面,不会影响定义在其他run loop的输入模式,即使它们的名字相同。

如果你的线程是基于Application Kti的话,主线程的run loop会自动运行,但是辅助线程(和只有Foundation的应用)必须自己启动它们的run loop。如果一个脱离线程没有进入run loop,那么线程在完成它们的方法执行后会立即退出。

尽管外表显式可能是线程安全的,但是NSRunLoop类是非线程安全的。你只能在拥有它们的线程里面调用它实例的方法。

Application Kit框架的线程安全

以下部分介绍了Application Kit框架的线程安全。

非线程安全类

以下这些类和函数通常是非线程安全的。大部分情况下,你可以在任何线程使用这些类,只要你在同一时间只有一个线程使用它们。查看这些类的文档来获得更多的详细信息。

  • NSGraphicsContext。多信息,参见“NSGraphicsContext 限制”。
  • NSImage.更多信息,参见“NSImage 限制”。
  • NSResponder。
  • NSWindow和所有它的子类。更多信息,参见“Window 限制

只能用于主线程的类

以下的类必须只能在应用的主线程使用。

  1. NSCell和所有它的子类。
  2. NSView和所有它的子类。更多信息,参见“NSView 限制”。

Window 限制

你可以在辅助线程创建一个window。Application Kit确保和window相关的数据结构在主线程释放来避免产生条件。在同时包含大量windows的应用中,window对象有可能会发生泄漏。

你也可以在辅助线程创建modal window。在主线程运行modal loop时,Application Kit阻塞辅助线程的调用。

事件处理例程限制

应用的主线程负责处理事件。主线程阻塞在NSApplication的run方法,通常该方法被包含在main函数里面。在Application Kit继续工作时,如果其他线程被包含在事件路径,那么操作有可能打乱顺序。比如,如果两个不同的线程负责关键事件,那么关键事件有可能不是按照顺序到达。通过让主线程来处理事件,事件可以被分配到辅助线程由它们处理。

你可以在辅助线程里面使用NSApplication的postEvent:atStart方法传递一个事件给主线程的事件队列。然而,顺序不能保证和用户输入的事件顺序相同。应用的主线程仍然辅助处理事件队列的事件。

绘画限制

Application Kit在使用它的绘画函数和类时通常是线程安全的,包括NSBezierPath和NSString类。关于使用这些类的详细信息,在以下各部分介绍。关于绘画的额外信息和线程可以查看Cocoa Drawing Guide。

a)  NSView限制

NSView通常是线程安全的,包含几个异常。你应该仅在应用的主线程里面执行对NSView的创建、销毁、调整大小、移动和其他操作。在其他辅助线程里面只要你把绘画的代码放在lockFocusIfCanDraw和unlockFocus方法之间也是线程安全的。

如果应用的辅助线程想要告知主线程重绘视图,一定不能在辅助线程直接调用display,setNeedsDisplay:,setNeedsDisplayInRect:,或setViewsNeedDisplay:方法。相反,你应该给给主线程发生一个消息让它调用这些方法,或者使用performSelectorOnMainThread:withObject:waitUntilDone:方法。

系统视图的图形状态(gstates)是基于每个线程不同的。使用图形状态可以在单线程的应用里面获得更好的绘画性能,但是现在已经不是这样了。不正确使用图形状态可能导致主线程的绘画代码更低效。

b)  NSGraphicsContext 限制

NSGraphicsContext类代表了绘画上下文,它由底层绘画系统提供。每个NSGraphicsContext实例都拥有它独立的绘画状态:坐标系统、裁剪、当前字体等。该类的实例在主线程自动创建自己的NSWindow实例。如果你在任何辅助线程执行绘画操作,需要特定为该线程创建一个新的NSGraphicsContext实例。

如果你在任何辅助线程执行绘画,你必须手工的刷新绘画调用。Cocoa不会自动更新辅助线程绘画的内容,所以你当你完成绘画后需要调用NSGraphicsContext的flusGrahics方法。如果你的应用程序只在主线程绘画,你不需要刷新绘画调用。

c)  NSImage限制

线程可以创建NSImage对象,把它绘画到图片缓冲区,还可以把它传递给主线程来绘画。底层的图片缓存被所有线程共享。关于图片和如何缓存的更多信息,参阅Ccocoa Drawing Guide。

Core Data框架

Core Data框架通常支持多线程,尽管需要注意一些使用注意事项。关于这些注意事项的更多信息,参阅Core Data Programing Guide的“Multi-Threading with Core Data”部分。

二、Core Foundation(核心框架)

Core Foundation是足够线程安全的,如果你的程序注意一下的话,应该不会遇到任何线程竞争的问题。通常情况下是线程安全的,比如当你查询(query)、引用(retain)、释放(release)和传递(pass)不可变对象时。甚至在多个线程查询*共享对象也是线程安全的。

像Cocoa那样,当涉及对象或它们内容突变时,Core Foundation是非线程安全的。比如,正如你所期望的,无论修改一个可变数据或可变数组对象,还是修改一个可变数组里面的对象都是非线程安全的。其中一个原因是性能,这是在这种情况下的关键。此外,在该级别上实现完全线程安全是几乎不可能的。例如,你不能排除从集合中引用(retain)一个对象产生的无法确定的结果。该集合本身在被调用来引用(retain)它所包含的对象之前有可能已经被释放了。

这些情况下,当你的对象被多个线程访问或修改,你的代码应该在相应的地方使用锁来保护它们不要被同时访问。例如,枚举Core Foundation数组对象的代码,在枚举块代码周围应该使用合适的锁来保护它免遭其他线程修改。

三、术语表

应用(application)

一个显示一个图形用户界面给用户的特定样式程序。

条件(condition)

一个用来同步资源访问的结构。线程等待某一条件来决定是否被允许继续运行,直到其他线程显式的给该条件发送信号。

临界区(critical section)

同一时间只能不被一个线程执行的代码。

输入源(input source)

一个线程的异步事件源。输入源可以是基于端口的或手工触发,并且必须被附加到某一个线程的run loop上面。

可连接的线程(join thread)

退出时资源不会被立即回收的线程。可连接的线程在资源被回收之前必须被显式脱离或由其他线程连接。可连接线程提供了一个返回值给连接它的线程。

主线程(main thread)

当创建进程时一起创建的特定类型的线程。当程序的主线程退出,则程序即退出。

互斥锁(mutex)

提供共享资源互斥访问的锁。一个互斥锁同一时间只能被一个线程拥有。试图获取一个已经被其他线程拥有的互斥锁,会把当前线程置于休眠状态知道该锁被其他线程释放并让当前线程获得。

操作对象(operation object)

NSOperation类的实例。操作对象封装了和某一任务相关的代码和数据到一个执行单元里面。

操作队列(operation queue)

NSOperationQueue类的实例。操作队列管理操作对象的执行。

进程(process)

应用或程序的运行时实例。一个进程拥有独立于分配给其他程序的的内存空间和系统资源(包括端口权限)。进程总是包含至少一个线程(即主线程)和任意数量的额外线程。

程序(program)

可以用来执行某些任务的代码和资源的组合。程序不需要一个图形用户界面,尽管图形应用也被称为程序。

递归锁(recursive lock)

可以被同一线程多次锁住的锁。

Run loop(运行循环)

一个事件处理循环,在此期间事件被接收并分配给合适的处理例程。

Run loop模式(run loop mode)

与某一特定名称相关的输入源、定时源和run loop观察者的集合。当运行在某一特定“模式”下,一个run loop监视和该模式相关的源和观察者。

Run loop对象(run loop object)

NSRunLoop类或CFRunLoopRef不透明类型的实例。这些对象提供线程里面实现事件处理循环的接口。

Run loop观察者(run loop observer)

在run loop运行的不同阶段时接收通知的对象。

信号量(semaphore)

一个受保护的变量,它限制共享资源的访问。互斥锁(mutexes)和条件(conditions)都是不同类型的信号量。

任务(task)

要执行的工作数量。尽管一些技术(最显著的是Carbon 多进程服务—Carbon Multiprocessing Services)使用该术语的意义有时不同,但是最通用的用法是表明需要执行的工作数量的抽象概念。

线程(thread)

进程里面的一个执行过程流。每个线程都有它自己的栈空间,但除此之外同一进程的其他线程共享内存。

定时源(timer source)

为线程同步事件的源。定时器产生预定时间将要执行的一次或重复事件。

四、结束语

多线程编程在开发应用的时候非常有帮助。比如你可以在后台加载图片,等图片加载完成后再在主线程更新等,或者在后台处理一些需要占用CPU很长时间的事件(比如请求服务器,加载数据等)。要体会多线程编程的好处,还得多实战,结合使用多种多线程技术。特别要注意Run Loop的使用,很多开发者在编写多线程应用的时候很少关注过Run Loop。如果你仔细阅读并掌握Run Loop的细节,将会帮助你写出更优美的代码。同步是多线程编程的老生常谈,估计大学时候大家都基本熟悉了同步的重要性。

最后,本文在翻译过程中发现很多地方直译成中文比较晦涩,所以采用了意译的方式,这不可避免的造成有一些地方可能和原文有一定的出入,所以如果你阅读的时候发现有任何的错误都可以讨论指正。