Threading Programming Guide · Thread Safety Summary [翻译]
原文链接
本附录描述了 OS X 和 iOS 中一些关键框架和高级线程安全性。奔赴路中的资料可能会有所修改。
Cocoa
使用多线程 Cocoa 的指南包括:
- 不可变对象通常是线程安全的。一旦创建了这些对象,就可以安全地将这些对象传递给线程或从线程传递给线程。另一方面,可变对象通常不是线程安全的。要在线程化应该程序中使用可变对象,应用必须适当地同步。更多信息,参见 《Mutable Versus Immutable》。
- 许多被认为「线程不安全」的对象只有在多线程中使用才不安全。这些对象中的许多对象可以从任何线程中使用,只要每次只有一个线程。特定限制于应用程序主线程的对象都这样调用。
- 应用的主线程负责处理事件。尽管如果事件路径中设计其他线程,ApplicationKit 将继续工作,但是操作可能会发生顺序错误。
- 如果你想使用一个线程来绘制视图,所有绘图代码都在 NSView 中的
lockFocusIfCanDraw
和unlockFocus
方法之间实现。 - 要将 POSIX 线程与 Cocoa 一起使用,必须首先将 Cocoa 置于多线程模式。有关更多信息,参见 《Using POSIX Threads in a Cocoa Application》。
Foundation 框架线程安全
有一种误解认为 Foundation 框架是线程安全的,而应用程序工具包框架则不是。不幸的是,这是一个粗略的概括,而且有些误导。每个框架都有线程安全的区域和非线程安全的区域。下面几节描述 Foundation 框架的一般线程安全性。
线程安全的类和函数
以下类和函数通常被认为是线程安全的。你可以从多个线程使用相同的示例,而无需先获取锁。
- NSArray
- NSAssertionHandler
- NSAttributedString
- NSBundle
- NSCalendar
- NSCalendarDate
- NSCharacterSet
- NSConditionLock
- NSConnection
- NSData
- NSDate
- NSDateFormatter
- NSDecimal functions
- NSDecimalNumber
- NSDecimalNumberHandler
- NSDeserializer
- NSDictionary
- NSDistantObject
- NSDistributedLock
- NSDistributedNotificationCenter
- NSException
- NSFileManager
- NSFormatter
- NSHost
- NSJSONSerialization
- NSLock
- NSLog/NSLogv
- NSMethodSignature
- NSNotification
- NSNotificationCenter
- NSNumber
- NSNumberFormatter
- NSObject
- NSOrderedSet
- NSPortCoder
- NSPortMessage
- NSPortNameServer
- NSProgress
- NSProtocolChecker
- NSProxy
- NSRecursiveLock
- NSSet
- NSString
- NSThread
- NSTimer
- NSTimeZone
- NSUserDefaults
- NSValue
- NSXMLParser
- Object allocation and retain count functions
- Zone and memory functions
线程不安全的类
一下类和函数不是线程安全的。在大多数情况下,只要每次在一个线程中使用这些类,就可以在任何线程中使用它们。查看类文档了解更多细节。
- NSArchiver
- NSAutoreleasePool
- NSCoder
- NSCountedSet
- NSEnumerator
- NSFileHandle
- NSHashTable functions
- NSInvocation
- NSMapTable functions
- NSMutableArray
- NSMutableAttributedString
- NSMutableCharacterSet
- NSMutableData
- NSMutableDictionary
- NSMutableOrderedSet
- NSMutableSet
- NSMutableString
- NSNotificationQueue
- NSPipe
- NSPort
- NSProcessInfo
- NSRunLoop
- NSScanner
- NSSerializer
- NSTask
- NSUnarchiver
- NSUndoManager
- User name and home directory functions
注意,虽然 NSArchiver
、NSCoder
、NSEnumerator
对象本身是线程安全的,但在这里列出它们是因为使用它们更改它们包装的数据对象是不安全的。例如,在归档的情况下,更改正在归档的对象图是不安全的。对于枚举器,任何线程更改枚举集合都是不安全的。
主线程独享类
以下类只能在应用的主线程使用。
可变和不可变
不可变对象通常是线程安全的;一旦创建了这些对象,就可以安全地将这些对象传递给线程或从线程传递给线程。当然,在使用不可变对象时,仍然需要记住正确地使用引用计数。如果不适当地释放没有保留的对象,可能会在稍后发生异常。
可变对象通常不是线程安全的。要在线程化应用中使用可变对象,应用必须使用锁同步对它们的访问。(有关更多信息,参见 《Atomic Operations》)。通常,当涉及到突变时,集合类(例如 NSMutableArray、NSMutableDictionary)不是线程安全的。也就是说,如果一个或多个线程正在更改一个数组,可能会出现问题。必须锁定读和写发生的位置,以确保线程安全。
即使一个方法声称返回一个不可变对象,也不应该简单地假设返回的对象是不可变的。根据方法实现的不同,返回的对象可能是可变的或不可变的。例如,返回类型为 NSString 的方法可能由于其实现而实际返回 NSMutableString。如果你想确保你拥有的对象是不可变的,你应该创建一个不可变的副本。
可重入性
只有当操作「调用」同一对象或不同对象中的其他操作时,才有可能实现重入。保留和释放对象就是这样一个有时被忽略的「调用」。
下表列出了 Foundation 框架中显式可重入的部分。所有其他类可能是可重入的,也可能不是,也可能在将来被重入。从来没有对可重入性进行过完整的分析,这个列表可能不是详尽的。
- Distributed Objects
- NSConditionLock
- NSDistributedLock
- NSLock
- NSLog/NSLogv
- NSNotificationCenter
- NSRecursiveLock
- NSRunLoop
- NSUserDefaults
类的初始化
Objective-C 运行时系统在类接收任何其他消息之前向各个类对象发送初始化消息。这使类有机会在使用之前设置其运行时的环境。在多线程应用中,运行时保证只有一个线程执行 initialize 方法 —— 恰好向类发送第一个消息的线程。如果第二个线程试图在第一个线程仍然位于 initialize 方法中时向类发送消息,那么第二个线程将阻塞,直到 initialize 方法执行完毕。同时,第一个线程可以继续调用该类上的其他方法。初始化方法不应依赖于该类的第二个线程调用方法;如果是这样,两个线程就会陷入死锁。
由于 OS X 10.1.x 及更早的一个 bug,一个线程可以在另一个线程完成执行该类的 initialize 方法之前向该类发送消息。然后线程可以访问未完全初始化的值,这可能会导致应用崩溃。如果遇到这个问题,你需要引入锁来防止在初始化之前访问这些值,或者在成为多线程之前强制类初始化本身。
自动释放池
每个线程维护自己的 NSAutoreleasePool 对象堆栈。Cocoa 希望在当前线程的堆栈上始终有一个可用的自动释放池。如果池不可用,则不会释放对象,并且会泄露内存。NSAutoreleasePool 对象根据 ApplicationKit 在应用的主线程中自动创建和销毁,但是辅助线程(以及仅用于 Foundation 的应用)必须在使用 Cocoa 之前创建他们自己的对象。如果线程的生命周期很长,并且可能生成许多自动释放对象,那么应该定期销毁并创建自动释放池(就像 ApplicationKit 在主线程所做的那样);否则,自动释放的对象会累积,内存占用会增加。如果分离的线程不使用 Cocoa,则不需要创建自动释放池。
RunLoop
每个线程有且仅有一个 RunLoop。但是每个 RunLoop 进而每个线程都有自己的一组输入模式,这些模式决定 RunLoop 侦听哪些输入源。在一个 RunLoop 中定义的输入模式不会影响在另一个 RunLoop 中定义的输入模式,即使它们可能具有相同的名称。
如果你的应用基于 ApplicationKit,那么主线程的 RunLoop 将自动运行,但是辅助线程(以及仅用于 Foundation 的应用)必须自己运行 RunLoop。如果分离的线程没有进入 RunLoop,那么一旦分离的方法执行完毕,线程就会退出。
尽管有一些外部表现,NSRunLoop 类并不是线程安全的。你应该仅从拥有该类的线程调用该类的实际方法。
ApplicationKit 框架线程安全
下面几节描述 ApplicationKit 框架的一般线程安全性。
线程不安全类
以下类和函数通常不是线程安全的。大多数情况下,只要每次只在一个线程中使用这些类,就可以在任何线程中使用它们。查看类文档了解更多细节。
- NSGraphicsContext。跟多信息参见 《NSGraphicsContext Restrictions》。
- NSImage。更多信息参见 《NSImage Restrictions》。
- NSResponder
- NSWindow 及其后代。更多信息参见 《Window Restrictions》。
主线程专用类
以下类只能在应用的主线程中使用。
- NSCell 及其后代
- NSView 及其后代。更多信息参见 《NSView Restrictions》。
窗口限制
你可以在辅助线程上创建一个窗口。ApplicationKit 确保在主线程上释放和窗口关联的数据结构,以避免争用条件。在同时处理大量窗口的应用中,窗口对象有可能泄露。
你可以在辅助线程上创建一个模态窗口。当主线程运行模态循环时,ApplicationKit 阻塞调用辅助线程。
事件处理限制
应用程序的主线程负责处理事件。主线程是在 NSApplication 的运行方法中阻塞的线程,通常在应用的 main 函数中调用。如果事件路径中涉及其他线程,则 ApplicationKit 将继续工作,但操作可能会发生顺序错误。例如,如果两个不同的线程正在响应关键事件,你可以获得更一致的用户体验。一旦接收到事件,如果需要,可以将事件分派给辅助线程进行下一步处理。
你可以从辅助线程调用 NSApplication 的 postEvent:atStart:
方法将时间发布到主线程的事件队列。但是,不能保证用户输入事件的顺序。应用的主线程仍然负责处理事件队列中的事件。
绘图限制
当使用图形函数及类(包括 NSBezierPath 和 NSString 类)绘图时,ApplicationKit 通常是线程安全的。下面几节将描述使用特定类的详细信息。有关绘图和线程的更多信息可在 《Cocoa Drawing Guide》 中获得。
NSView 限制
NSView 类通常不是线程安全的。你应该仅从应用的主线程中创建、销毁、调整大小、移动 NSView 对象并对其执行其他操作。只要将绘图调用与 lockFocusIfCanDraw
和 unlockFocus
对应起来,从辅助线程进行绘图就是线程安全的。
如果应用的辅助线程想要在主线程上重新绘制视图的某些部分,则必须使用 display
、setNeedsDisplay:
、setNeedsDisplayInRect:
或 setViewsNeedDisplay:
等方法。相反,它应该向主线程发送一条消息,或者使用 performSelectorOnMainThread:withObject:waitUntilDone:
方法取而代之。
视图系统的图形状态(gstates)是每个线程的。使用图形状态曾经是在单线程应用上实现更好绘图性能的一种方法,但现在不再如此。不正确地使用图形状态实际上会导致绘图代码效率低于在主线程中绘图。
NSGraphicsContext 限制
NSGraphicsContext 类表示底层图形系统提供的绘图上下文。每个 NSGraphicsContext 实例都拥有自己独立的图形状态:坐标系统、裁剪、当前自己等等。类的实例在主线程上为每个 NSWindow 示例自动创建。如果从辅助线程进行任何绘图,则会为该线程专门创建一个 NSGraphicsContext 的新实例。
如果从辅助线程进行绘图,则必须手动刷新绘图调用。Cocoa 不会自动从辅助线程中绘制的内容更新视图,所以你需要在完成绘图时调用 NSGraphicsContext 的 flushGraphics
方法。如果应用只从主线程中绘制内容,则不需要刷新绘图调用。
NSImage 限制
一个线程可以创建一个 NSImage 对象,绘制到图像缓冲区,并将其传递给住线程进行绘制。底层映像缓存在所有线程之间共享。有关图像和缓存如何工作的更多信息,参见 《Cocoa Drawing Guide》。
CoreData 框架
CoreData 框架通常支持线程,不过也有一些使用上的注意事项。有关这些警告的信息,参阅 《Concurrency with Core Data》 和 《Core Data Programming Guide》。
Core Foundation
CoreFoundation 具有足够的线程安全性,如果你小心编程,就不会遇到与竞争线程相关的任何问题。在常见的情况下,例如查询、保留、释放和传递不可变的对象时,它是线程安全的。甚至可能从多个线程查询的中信共享对象也是可靠的线程安全的。
与 Cocoa 一样,CoreFoundation 在对象或其内容发生突变时也不是线程安全的。例如,修改可变数据或可变数组对象并不像你期望的那样是线程安全的,但是修改不可变数组中的对象也不是线程安全的。其中一个原因是性能,这在这些情况下非常关键。此外,在这个级别通常不可能实现绝对线程安全。例如,你不能排除由于保留从集合中获得的对象而导致的不确定行为。在调用保留包含的对象之前,可以释放集合本身。
在需要从多个线程访问 CoreFoundation 对象并对其进行修改的情况下,你的代码应该使用访问点上的锁来防止并发访问。例如,枚举 CoreFoundation 数组对象的代码应该在枚举块周围使用适当的锁定调用,以防止其他人更改数组。