iOS线程安全问题

此文章将侧重于编写线程安全类和使用Grand Central Displatch(GCD)时的实用的技巧,设计模式,以及反模式。

线程安全

Apple的框架

首先让我们来看一下Apple的框架。一般情况下,除非提前声明,否则大多数类默认不是线程安全的。一些是我们所期望的,但是另一些却会相当有趣。

其中甚至有经验的iOS/Mac开发人员常会犯的错误是在后台线程中访问部分UIKit/AppKit。最容易犯的错误是在后台线程中对property赋值,比如图片,因为他们的内容是在后台从网络上获取的。Apple的代码是性能优化过的,如果你从不同线程去改动property,它是不会警告你的。

例如图片这种情况,一个常见的问题是你的改动会产生延迟。但是如果两个线程同时设置图片,很可能你的程序将直接崩溃,因为当前设置的图片可能会被释放两次。由于这是和时机相关的,因此崩溃通常发生在客户使用时,而并不是在开发过程中。

虽然没有官方的工具来发现这样的错误,但是有一些技巧可以避免这种错误发生。The UIKit Main Thread Guard是一小段代码,可以修补任何调用UIView的setNeedsLayout和setNeedsDisplay,以及在发送调用之前检查是否执行在主线程。由于这两种方法被许多UIKit的setters方法调用(包括图片),这将会捕获许多线程相关的错误。虽然这个不使用私有API,但是我们不建议在产品程序中使用,而是最好在开发过程是使用。

UIKit非线程安全是Apple有意的设计决定。从性能方面来说线程安全没有太多好处,它实际上会使很多事情变慢。而事实上UIKit和主线程捆绑使它很容易编写并发程序和使用UIKit。你所需要做的就是确保总是在主线程上调用UIKit。

为什么UIKit不是线程安全的?

像UIKit这样大的框架上确保线程安全是一个重大的任务,会带来巨大的成本。改变非原子property为原子property只是所需要改变的一小部分。通常你想要一次改变多个property,然后才能看到更改的结果。对于这一点,Apple不得不暴露一个方法,像CoreData的performBlock:和同步的方法performBlockAndWait:。如果你考虑大多数调用UIKit类是有关配置(configuration),使他们线程安全更没有意义。

然而,即使调用不是关于配置(configuration)来共享内部状态,因此它们不是线程安全的。如果你已经写回到黑暗时代iOS3.2及以前的应用程序,你一定经历过当准备背景图像时使用NSString的drawInRect:withFont:随时崩溃。值得庆幸的是随着iOS4的到来,Apple提供了大部分绘图的方法和类,例如UIColor和UIFont在后台线程中的使用。

不幸的是,Apple的文档目前还缺乏有关线程安全的主题。他们建议只在主线程访问,甚至连绘画方法他们都不能保证线程安全。所以阅读iOS的版本说明总是一个好主意。

在大多数情况下,UIKit类只应该在程序的主线程使用。无论是从UIResponder派生的类,还是那些涉及以任何方式操作你的应用程序的用户界面。

解除分配问题

另一个在后台使用UIKit对象的风险是“解除分配问题”。Apple在TN2109里概括了这个问题,并提出了多种解决方案。这个问题是UI对象应该在主线程中释放,因为一部分对象有可能在dealloc中对视图层次结构进行更改。正如我们所知,这种对UIKit的调用需要发生在主线程上。

由于它常见于次线程,操作或块保留调用者,这很容易出错,并且很难找到并修复。这也是在AFNetworking中长期存在的一个bug,只是因为不是很多人知道这个问题,照例,显然它很罕见,并且很难重现崩溃。在异步块操作里一贯使用__weak和不访问ivars会有所帮助。

集合类

Apple有一个很好的概述文档,对iOS和Mac上列出线程安全最常见的基础类。一般情况下,不可变类,像NSArray是线程安全的,而它们的可变的变体,像NSMutableArray则不是。事实上,当在一个队列中序列化的访问时,是可以在不同线程中使用它们的。请记住,方法可能返回一个集合对象的可变变体,即使它们生命它们的返回类型是不可变的。好的做法是写一些像return [array copy]来确保返回的对象实际上是不可变的。

不同于像Java语言,Foundation框架不提供框架外的线程安全的集合类。其实这是非常合理的,因为在大多数情况下,你想在更高层使用你的锁去避免过多的锁操作。一个值得注意的例外是缓存,其中一个可变的字典可能会保存不变的数据-在这里Apple在iOS4中增加了NSCache,它不仅能锁定访问,还可以在低内存情况下清除它的内容。

这就是说,在你的程序中,这也许是有效的情况,其中一个线程安全的可变的字典可以很轻便的。而这要归功于类簇(class cluster)的解决方案,它可以很容易的写一个。

原子属性(properties)

有没有想过Apple如何处理原子设置/获取属性?现在你可能已经听说过spinlocks, semaphores, locks, @synchronized – 那Apple使用什么?幸运的是,Objective-C运行是公开的,所以我们可以看看幕后发生了什么。

一个非原子属性的setter方法可能看起来像这样:

  1. - (void)setUserName:(NSString *)userName {
  2. if (userName != _userName) {
  3. [userName retain];
  4. [_userName release];
  5. _userName = userName;
  6. }
  7. }

这是手动retain/release变量,然而用ARC生成的代码看起来类似。让我们看看这段代码,很显然当setUserName:被同时调用就遇到了麻烦。我们最终可能会释放_userName两次,这会破坏内存,并且导致难以发现的bug。

对于任意一个非手工实现的property内部发生的是,编译器生成一个调用objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)。在我们的例子中,调用参数是这样的:

  1. objc_setProperty_non_gc(self, _cmd,
  2. (ptrdiff_t)(&_userName) - (ptrdiff_t)(self), userName, NO, NO);

ptrdiff_t你可能看起来很怪异,但最终它是一个简单的指针算法,因为一个Objective-C类正是另一个C结构。

objc_setProperty调用下面的方法:

  1. static inline void reallySetProperty(id self, SEL _cmd, id newValue,
  2. ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
  3. {
  4. id oldValue;
  5. id *slot = (id*) ((char*)self + offset);
  6. if (copy) {
  7. newValue = [newValue copyWithZone:NULL];
  8. } else if (mutableCopy) {
  9. newValue = [newValue mutableCopyWithZone:NULL];
  10. } else {
  11. if (*slot == newValue) return;
  12. newValue = objc_retain(newValue);
  13. }
  14. if (!atomic) {
  15. oldValue = *slot;
  16. *slot = newValue;
  17. } else {
  18. spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
  19. _spin_lock(slotlock);
  20. oldValue = *slot;
  21. *slot = newValue;
  22. _spin_unlock(slotlock);
  23. }
  24. objc_release(oldValue);
  25. }

除了相当有趣的名字,这种方法其实是相当简单,并使用128个在PropertyLocks可用的spinlocks其中之一。这是一个务实的和快速的解决方案 – 最坏的情况是,因为一个哈希冲突,一个setter不得不等待一个不相关的setter结束。

虽然这些方法在任何公共头文件都没有声明,但可以手动调用它们。我并不是说这是一个好主意,但如果你想要原子属性和想要同时实现setter,知道这些是很有趣的并且可能会相当有用。

  1. // Manually declare runtime methods.
  2. extern void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset,
  3. id newValue, BOOL atomic, BOOL shouldCopy);
  4. extern id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset,
  5. BOOL atomic);
  6. #define PSTAtomicRetainedSet(dest, src) objc_setProperty(self, _cmd,
  7. (ptrdiff_t)(&dest) - (ptrdiff_t)(self), src, YES, NO)
  8. #define PSTAtomicAutoreleasedGet(src) objc_getProperty(self, _cmd,
  9. (ptrdiff_t)(&src) - (ptrdiff_t)(self), YES)

参考这个gist全部片段包括处理结构的代码。但是请记住我们不建议使用这个。

@synchronized如何?

你可能很好奇为什么Apple不使用一个已有的运行时特性@synchronized(self)来做属性锁。一旦你看了源代码,你将明白这还有很多事要做。Apple采用最多三个上锁/解锁序列,部分原因是他们还增加了异常展开(exception unwinding)。比起更加快速的spinlock方案,这个会慢一些。由于设置属性通常是相当快的,spinlocks是最完美的选择。当你需要确保没有代码死锁而抛出异常,@synchronized(self)是个好的选择。

你自己的类

单独使用原子属性不会让你的类线程安全的。它只会保护你在setter中免受竞态条件(race conditions),但不会保护你的应用程序逻辑。请考虑以下代码片段:

  1. if (self.contents) {
  2. CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
  3. (__bridge CFStringRef)self.contents, NULL);
  4. // draw string
  5. }

我在PSPDFKit早早就犯了这个错误。偶尔,当contents属性检查后被设置为nil,该应用程序以EXC_BAD_ACCESS崩溃了。对这个问题简单的解决办法是捕获变量:

  1. NSString *contents = self.contents;
  2. if (contents) {
  3. CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
  4. (__bridge CFStringRef)contents, NULL);
  5. // draw string
  6. }

这样就解决了问题,但在大多数情况下,它不是那么简单的。试想一下,我们也有一个textColor属性,我们在一个线程中改变两次属性。那么,我们的渲染线程可能最终会使用有旧颜色值的新内容,我们得到一个奇怪的组合。这就是为什么Core Data在一个线程或队列中绑定模型对象。

对于这个问题没有一个统一标准的解决方案。使用不可变的模型是一个解决方案,但它有它自己的问题。另一种方法是限制在主线程或一个特定的队列更改现有对象,而在工作线程中使用之前生成的副本。我推荐Jonathan Sterling在文章中为解决这个问题更多的想法。

简单的解决方法是使用@synchronize。其他的是非常,非常有可能让你陷入困境。更聪明的人一次又一次地在其他方法上失败了。

实用的线程安全设计

在试图做线程安全之前,认真考虑是否是必要的。请确保它不是过早的优化。如果它像是一个配置类,考虑线程安全是没有意义的。更好的方法是抛出一些断言来确保它的正确使用:

  1. void PSPDFAssertIfNotMainThread(void) {
  2. NSAssert(NSThread.isMainThread,
  3. @"Error: Method needs to be called on the main thread. %@",
  4. [NSThread callStackSymbols]);
  5. }

现在肯定有线程安全的代码,一个很好的例子就是缓存类。一个好的方法是使用一个并行dispatch_queue为读/写锁,以最大限度地提高性能,并尝试只锁定那些真正需要的地方。一旦你开始使用多个队列用于锁定不同部位,事情将很快变得棘手。

有时候,你也可以重写你的代码,使特殊的锁不是必需的。考虑这个代码片段,是一个多播委托的形式。 (在许多情况下,使用NSNotifications会更好,但也有有效的多路广播委托用例。)

  1. // header
  2. @property (nonatomic, strong) NSMutableSet *delegates;
  3. // in init
  4. _delegateQueue = dispatch_queue_create("com.PSPDFKit.cacheDelegateQueue",
  5. DISPATCH_QUEUE_CONCURRENT);
  6. - (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
  7. dispatch_barrier_async(_delegateQueue, ^{
  8. [self.delegates addObject:delegate];
  9. });
  10. }
  11. - (void)removeAllDelegates {
  12. dispatch_barrier_async(_delegateQueue, ^{
  13. self.delegates removeAllObjects];
  14. });
  15. }
  16. - (void)callDelegateForX {
  17. dispatch_sync(_delegateQueue, ^{
  18. [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
  19. // Call delegate
  20. }];
  21. });
  22. }

除非addDelegate:或removeDelegate:每秒被调用上千次,否则下面是更简洁的方法:

  1. // header
  2. @property (atomic, copy) NSSet *delegates;
  3. - (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
  4. @synchronized(self) {
  5. self.delegates = [self.delegates setByAddingObject:delegate];
  6. }
  7. }
  8. - (void)removeAllDelegates {
  9. self.delegates = nil;
  10. }
  11. - (void)callDelegateForX {
  12. [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
  13. // Call delegate
  14. }];
  15. }

当然,这个例子有点儿认为构造的,它可以简单的局限于在主线程更改。但对于许多数据结构,在修改方法中创建不可变的副本是值得的,让广大的应用程序逻辑并不需要处理过多的锁定。注意,我们仍然要在addDelegate:申请锁,否则如果委托对象被来自不同的线程同时调用,它可能会迷失。

GCD的陷阱

对于大部分的锁定需求,GCD是完美的。这很简单,很快速,并且它的基于块的API使得它更难偶然做出不平衡锁。不过,也有不少缺陷,我们将要在这里探索其中一些。

使用GCD作为递归锁

GCD是一个队列来序列化访问共享资源。这可以被用于锁定,但它比@synchronized大不相同。 GCD队列是不可重入的 – 这将打破队列特性。许多人试图使用dispatch_get_current_queue()来作为替代方案,这是一个坏主意。Apple在iOS6中废弃此方法自然有它的原因。

  1. // This is a bad idea.
  2. inline void pst_dispatch_sync_reentrant(dispatch_queue_t queue,
  3. dispatch_block_t block)
  4. {
  5. dispatch_get_current_queue() == queue ? block()
  6. : dispatch_sync(queue, block);
  7. }

测试当前队列简单的解决方案可能起作用,但当你的代码变得更加复杂的时候,你可能会在同一时间对多个队列上锁,它会失败。一旦你是这种情况,你几乎肯定会遇到死锁。当然,人们可以使用dispatch_get_specific(),它会遍历整个队列的层次结构来测试特定的队列。对于您将不得不编写应用此元数据的自定义队列的构造函数。不要走那条路,很多使用情况下,NSRecursiveLock是更好的解决方案。

dispatch_async的固定时序问题

在UIKit中有一些时序问题?大多数时候,这将是完美的“修复”:

  1. dispatch_async(dispatch_get_main_queue(), ^{
  2. // Some UIKit call that had timing issues but works fine
  3. // in the next runloop.
  4. [self updatePopoverSize];
  5. });

相信我,不要这样做。这将在以后缠着你因为你的应用程序变得越来越大。这是超级难调试,并因为“时序问题”当你需要调度越来越多,事情很快会土崩瓦解。看你的代码,找到适当调用的位置(例如viewWillAppear而不是viewDidLoad中)。在我的代码库仍然有一些黑客方式,但大部分都会被适当的记录并且提交问题。

请记住,这真不是GCD特有的,但它是一个常见的反模式,只是GCD很容易做到。你可以使用同样的才智performSelector:afterDelay:,其中下一个runloop的延迟是0.f。

在性能关键代码中使用混合dispatch_sync和dispatch_async

那个花了我一段时间才弄清楚。在PSPDFKit中有一个使用LRU列表来跟踪图像访问的缓存类。当你通过页面滚动,它会被调用很多次。最初的实现中对于可用的访问使用dispatch_sync,用dispatch_async来更新LRU位置。这导致帧速率远远低于每秒60帧的目标。

当你的应用程序中运行的其他代码阻止GCD的线程,它可能需要一段时间,直到调度管理器发现一个线程来执行dispatch_async代码 – 在那之前,你的同步调用将被阻塞。即使,在这个例子中,在异步情况下执行的顺序并不重要,没有简单的方法来告诉给GCD 。读/写锁在这里不会有任何帮助,因为异步流程非常肯定需要执行一个写屏障,在这期间你的所有读操作都会被锁定。教训:如果滥用, dispatch_async可以是昂贵的。使用它来锁操作要非常小心。

使用dispatch_async来调度内存密集型操作

我们已经谈了很多关于NSOperations ,而且使用更高层的API通常是一个好主意。如果你处理的是内存密集型操作的工作块,这是尤其如此。

在旧版本的PSPDFKit中,我用了一个GCD队列来调度写缓存JPG图像到磁盘。当视网膜的iPad出来了,这开始引起麻烦。分辨率加倍,比起渲染图像,对图像数据进行编码需要更长的时间。因此,操作堆积在队列中,当系统繁忙它可能会因为内存耗尽而崩溃。

没有办法来看到有多少操作在排队里(除非你手动添加代码来追踪这一点) ,而且也没有内置的方式来取消操作万一收到内存不足的通知。切换到NSOperations使代码更加可调试,并允许这一切都无需编写手动管理代码。

当然也有一些注意事项,例如你不能在你的NSOperationQueue上设置一个目标队列(如为节流的I/O而DISPATCH_QUEUE_PRIORITY_BACKGROUND ) 。但是,这是一个为可调试性付出的很小的代价,也防止你陷入类似问题,如优先级反转。我甚至建议使用漂亮的NSBlockOperation API,并建议NSOperation的真正子类,包括描述的实现。这是更多的工作,但后来,有一个方法出奇的有用是打印所有运行/挂起的操作。

时间: 2024-08-29 01:23:19

iOS线程安全问题的相关文章

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用

目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 多线程的四种解决方案:pthread,NSThread,GCD,NSOperation 线程安全问题 NSThread的使用 GCD的理解与使用 NSOperation的理解与使用 Demo在这里:WHMultiThreadDemo Demo的运行gif图如下: 一.多线程的基本概念 进程:可以理解成一个运行中的应用程序,是系统进行资源分配和调度的基本单位,是操作

iOS开发线程安全问题

先来看一下代码: - (void)viewDidLoad { [super viewDidLoad]; self.testStr = @"String initial complete"; [self performSelector:@selector(changeStr) withObject:nil afterDelay:0.5]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),

关于CoreData和SQLite多线程访问时的线程安全问题

http://www.jianshu.com/p/95db3fc4deb3 关于CoreData和SQLite多线程访问时的线程安全问题 数据库读取操作一般都是多线程访问的.在对数据进行读取时,我们要保证其当前状态不能被修改,即读取时加锁,否则就会出现数据错误混乱.IOS中常用的两种数据持久化存储方式:CoreData和SQLite,两者都需要设置线程安全,在这里以FMDB来解释对SQLite的线程安全访问. 一:FMDB的线程安全:(以读取图片为例) 1.没有线程安全的执行方式: //****

Core Data 的线程安全问题

前言: 很多小的App只需要一个ManagedContext在主线程就可以了,但是有时候对于CoreData的操作要耗时很久的,比如App开启的时候要载入大量数据,如果都放在主线程,毫无疑问会阻塞UI造成用户体验很差.通常的方式是,主线程一个ManagedContext处理UI相关 的,后台一个线程的ManagedContext负责耗时操作的,操作完成后通知主线程.使用CoreData的并行主要有两种方式: Notificaiton child/parent context 何时会使用到后台或者

iOS线程同步和锁

应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题.两个线程同时修改同一资源有可能以意想不到的方式互相干扰. iOS 提供了你可以使用的多个同步工具,从提供互斥访问你程序的有序的事件的工具等.以下个部分介绍了这些工具和如何在代码中使用他们来影响安全的访问程序的资源. 我们通过同一个例子来说明这些锁,当两个线程同时操作一个可变数组时,一个线程添加数据,一个线程删除数据,类似一个生产消费者模式,就会存在线程安全问题: 使用POSIX互斥锁 __block pthread_mutex_

java线程安全问题之静态变量、实例变量、局部变量

Java多线程编程中,存在很多线程安全问题,至于什么是线程安全呢,给出一个通俗易懂的概念还是蛮难的,如同<java并发编程实践>中所说: 写道 给线程安全下定义比较困难.存在很多种定义,如:"一个类在可以被多个线程安全调用时就是线程安全的". 此处不赘述了,首先给出静态变量.实例变量.局部变量在多线程环境下的线程安全问题结论,然后用示例验证,请大家擦亮眼睛,有错必究,否则误人子弟! 静态变量:线程非安全. 静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态

多线程创建方式及线程安全问题

1.创建线程方式 一:  创建线程方式一继承Thread类 public clsss MyThread extends Thread{ //重写run方法,设置线程任务 Run(){ } } main(){ new MyThread().start(); } 获取线程名称: Thread.currentThread()获取当前线程对象 Thread.currentThread().getName();获取当前线程对象的名称 二:创建线程方式-实现Runnable接口 创建线程的步骤. 1.定义类

javaweb学习总结二十三(servlet开发之线程安全问题)

一:servlet线程安全问题发生的条件 如果多个客户端访问同一个servlet时,发生线程安全问题,那么它们访问的是相同的资源.如果访问 的不是相同资源,则不存在线程安全问题. 实例1:不会产生线程安全问题,因为每个客户端发送请求,都会创建一个线程,都会创建一个count 不存在资源共享的问题. 1 public void doPost(HttpServletRequest request, HttpServletResponse response) 2 throws ServletExcep

javaweb回顾第六篇谈一谈Servlet线程安全问题

前言:前面说了很多关于Servlet的一些基础知识,这一篇主要说一下关于Servlet的线程安全问题. 1:多线程的Servlet模型 要想弄清Servlet线程安全我们必须先要明白Servlet实例是如何创建,它的模式是什么样的. 在默认的情况下Servlet容器对声明的Servlet,只创建一个Servlet实例,那么如果要是多个客户同时请求访问这个Servlet,Servlet容器就采取多线程.下面我们来看一幅图 从图中可以看出当客户发送请求的时候,Servlet容器通过调度者线程从线程池