iOS:应用程序的线程安全性

简介: 本文在于说明iOS应用的Objective-C代码的线程安全性。先是简单介绍一下线程安全的基本知识,然后通过一个小例子来观察非线程安全代码,最后会稍稍介绍一个可以用来分析线程安全隐患的工具。
本文在于说明iOS应用的Objective-C代码的线程安全性。先是简单介绍一下线程安全的基本知识,然后通过一个小例子来观察非线程安全代码,最后会稍稍介绍一个可以用来分析线程安全隐患的工具。

1) 基础知识 (Threading Basics)


当启动一个应用时,iOS会对应创建一个进程(process)和一块为之分配的内存。简单地说,一个应用进程的内存包括三个部分:  (更详细的描述可以看 这里 ):

程序内存( program memory)存储应用的执行代码,它在执行时由一个指令指针(Instruction Pointer, IP)来跟踪程序执行位置。

堆( heap )存储由 [… alloc] init]来创建的对象。

堆栈( stack )则用于函数调用。存储参数和函数的局部变量。

一个应用进程默认有一个主线程。如果有多线程,所有线程共享 program memory  和  heap  , 每个线程又有各自的IP和堆栈。就是说每个线程都有自己的执行流程,当它呼叫一个方法时,其它线程是无法访问调用参数和该方法的局部变量的。而那些在堆(heap)上创建的对象却可以被其它线程访问和使用。

2) 实验 (Experiment)


建个使用如下代码的小程序:
 @interface FooClass {}  
 @property (nonatomic, assign) NSUInteger value;  
 - (void)doIt;  
 @end  
   
 @implementation FooClass  
 @synthesize value;  
   
 - (void)doIt {  
      self.value = 0;  
      for (int i = 0; i < 100000; ++i) {  
           self.value = i;  
      }  
      NSLog(@"执行后: %d (%@)", self.value, [NSThread currentThread]);  
 }  
 @end

这个类有一个整型属性value,并且会在doIt方法被连续增加100000次。执行完后,再将它的值和调用doIt方法的线程信息输出出来。 如下在AppDelegate中增加一个 _startExperiment方法,然后在 application:didFinishLaunchingWithOptions:方法中调用它 :
 - (void)_startExperiment {  
      FooClass *foo = [[FooClass alloc] init];  
      [foo doIt];  
      [foo release];  
 }  
   
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
      // …  
      [self _startExperiment];  
      return YES;       
 }

因为这里还有多线程,所以结果很简单地显示value值为99999。

3) 线程安全 (Thread Safety)


如下以多线程并行执行doIt():
- (void)_startExperiment {  
      FooClass *foo = [[FooClass alloc] init];  
      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
     
   for (int i = 0; i < 4; ++i) {  //四个线程
     dispatch_async(queue, ^{  
       [foo doIt];  
     });  
   }  
   [foo release];  
 }

 
再执行,你的输出可能会类似如下的结果: (实际可能不一样):
 after execution: 19851 (NSThread: 0x6b29bd0>{name = (null), num = 3})  
 after execution: 91396 (NSThread: 0x6b298f0>{name = (null), num = 4})  
 after execution: 99999 (NSThread: 0x6a288a0>{name = (null), num = 5})  
 after execution: 99999 (NSThread: 0x6b2a6f0>{name = (null), num = 6})  

并不是每个线程的value都是99999。这是因为现在的代码并不是线程安全的。

所谓线程安全就是代码运行在多线程环境下和运行在单线程环境下是一样的。

是什么导致了这个行为呢? 正如前面所说的每个线程都有其自己的IP和堆栈,但却共享堆(heap)。例子中的FooClass是创建在堆上的,所有线程都可以使用。下图展示了两个线程在执行doIt方法时的冲突: :

Thread 1和Thread 2正在不同的位置执行。doIt()并没有对多线程的执行进行保护,它的实现是非线程安全的。

一个将doIt()变为线程安全的方式是在其函数体外使用如下编译指示符(directive):

新的代码如下所示:
- (void)doIt {  
   @synchronized(self) {  
     self.value = 0;   
     for (int i = 0; i < 100000; ++i) {  
       self.value = i;  
     }  
     NSLog(@"after execution: %d (%@)", self.value, [NSThread currentThread]);       
   }  
 } 

使用 @synchronized指示符 , 每个线程会在doIt()互斥地使用self。不过因为目前的代码中@synchronized包住了整个函数体,并不能达到并行执行的效果。

另一种同步访问机制是使用GCD: Grand Central Dispatch (GCD) .

 4) 如何识别非线程安全的代码 (How to identify not thread safe code)


上面例子太过于简单了。现实中,花了时间写好的代码,常常遇到死锁、崩溃,或者一些无法复现的问题。总之和期望的行为不一样。

线程问题的主因是共享或全局状态(state)数据。多个对象访问一个全局变量,或者在堆中分享了共同对象,再或者向共同的存储空间写入数据。在前面例子中所共享的状态是self, 对应的访问也就是 self.value。例子中所展示要比实际上的情况简单太多了,事实上确定使用的共享或全局状态(share or global state)并不容易。

解决方案就是写了一个工具,由多线程调用的函数来识别。下面是这个工具的核心概念。

工具主要包含了四个类: 
MultiThreadingAnalysis的实例用于记录一个线程对方法的调用,   ThreadingTrace类和 MethodExecution类用来输出 MultiThreadingAnalysis整理的分析结果 MultiThreadingAnalysisHook类则用于hook到对象并追踪它被调用的所有方法。




MultiThreadingAnalysis类提供两个方法 :
  • recordCallToMethod:ofClass:onThread: 记录某个方法在某个线程上被调用了。
  • threadingTraceOfLastApplicationRun 需要在分析完成后调用。
 @interface MultiThreadingAnalysis : NSObject  
   
      - (void)recordCallToMethod:(NSString*)methodName  
                ofClass:(NSString*)className  
               onThread:(NSString*)threadID;  
            
      - (ThreadingTrace*) threadingTraceOfLastApplicationRun;  
            
 @end  


分析结果由 ThreadingTrace来处理 . 它包含了一组 MethodExecution实例,每一个都表示了一个线程对一个方法的调用 :
 /*  
  * An instance of this class captures  
  * which methods of which classes have been  
  * called on which threads.  
  */  
 @interface ThreadingTrace : NSObject  
      /*  
       * Set of MethodExecution  
       */  
      @property (nonatomic, readonly) NSSet *methodExecutions;  
      - (void)addMethodExecution:(MethodExecution*)methodExec;  
 @end  
   
 /*  
  * An instance of this class represents a call  
  * to a method of a specific class on a thread  
  * with a specific threadID.  
  */  
 @interface MethodExecution : NSObject  
      @property (nonatomic, retain) NSString *methodName;  
      @property (nonatomic, retain) NSString *className;  
      @property (nonatomic, retain) NSString *threadID;  
 @end  

为了尽可能方法地记录方法的调用,我使用了NSProxy来hook对一个对象所有方法的调用。 MultiThreadingAnalysisHook类继承自 NSProxy,并在 forwardInvocation:  方法解析对target对象的调用. 在重定位到target对象前,会先使用一个 MultiThreadingAnalysis实例来记录下这次调用。
 
@interface MultiThreadingAnalysisHook : NSProxy  
      @property (nonatomic, retain) id target;  
      @property (nonatomic, retain) MultiThreadingAnalysis *analysis;  
 @end  
   
 @implementation MultiThreadingAnalysisHook  
   
 -(void)forwardInvocation:(NSInvocation*)anInvocation {  
     
   [self.analysis recordCallToMethod:NSStringFromSelector([anInvocation selector])  
                    ofClass:NSStringFromClass([self.target class])  
                onThread:[NSString stringWithFormat:@"%d", [NSThread currentThread]]];  
     
   [anInvocation invokeWithTarget:self.target];  
 }  
 @end

现在就可以使用了。在你要分析的类中创建一个私有方法 _withThreadingAnalysis  。 这个方法要创建一个 MultiThreadingAnalysisHook实例并且将target指到self。在自行指定的初始化函数中调用 _withThreadingAnalysis并返回其结果(HOOK的动作)。这样就达到使用 MultiThreadingAnalysisHook实例将原本对象的self封装起来,并可以记录所有外部对象的调用
 
@implementation YourClass  
   
 - (id)init {  
      //... do init stuff here  
      return [self _withThreadingAnalysis];  
 }  
   
 - (id)_withThreadingAnalysis {  
   MultiThreadingAnalysisHook *hook =   
     [[MultiThreadingAnalysisHook alloc] init];  
   hook.target = self;  
   return hook;  
 }  
 @end


此后就可以调用 MultiThreadingAnalysis   的 threadingTraceOfLastApplicationRun方法获取分析结果。最简单地输出到文本文件,结果如下:

begin threading analysis for class FooClass
   method doIt (_MultiThreadAccess_)
   method init (_SingleThreadAccess_)  

如果某个方法被多线程调用(标注为 _MultiThreadAccess_), 你可以看到更多详细信息。

转载请注明出处: http://blog.csdn.net/horkychen


目录
相关文章
|
2月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
237 4
|
3月前
|
设计模式 安全 Swift
探索iOS开发:打造你的第一个天气应用
【9月更文挑战第36天】在这篇文章中,我们将一起踏上iOS开发的旅程,从零开始构建一个简单的天气应用。文章将通过通俗易懂的语言,引导你理解iOS开发的基本概念,掌握Swift语言的核心语法,并逐步实现一个具有实际功能的天气应用。我们将遵循“学中做,做中学”的原则,让理论知识和实践操作紧密结合,确保学习过程既高效又有趣。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你打开一扇通往iOS开发世界的大门。
|
3月前
|
搜索推荐 IDE API
打造个性化天气应用:iOS开发之旅
【9月更文挑战第35天】在这篇文章中,我们将一起踏上iOS开发的旅程,通过创建一个个性化的天气应用来探索Swift编程语言的魅力和iOS平台的强大功能。无论你是编程新手还是希望扩展你的技能集,这个项目都将为你提供实战经验,帮助你理解从构思到实现一个应用的全过程。让我们开始吧,构建你自己的天气应用,探索更多可能!
81 1
|
1月前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
2月前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
2月前
|
安全 Swift iOS开发
Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法
本文深入探讨了 Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法。Swift 以其简洁、高效和类型安全的特点,结合 UIKit 丰富的组件和功能,为开发者提供了强大的工具。文章从 Swift 的语法优势、类型安全、编程模型以及与 UIKit 的集成,到 UIKit 的主要组件和功能,再到构建界面的实践技巧和实际案例分析,全面介绍了如何利用这些技术创建高质量的用户界面。
41 2
|
2月前
|
JSON 前端开发 API
探索iOS开发之旅:打造你的第一个天气应用
【10月更文挑战第36天】在这篇文章中,我们将踏上一段激动人心的旅程,一起构建属于我们自己的iOS天气应用。通过这个实战项目,你将学习到如何从零开始搭建一个iOS应用,掌握基本的用户界面设计、网络请求处理以及数据解析等核心技能。无论你是编程新手还是希望扩展你的iOS开发技能,这个项目都将为你提供宝贵的实践经验。准备好了吗?让我们开始吧!
|
2月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户点击按钮时,按钮将从圆形变为椭圆形,颜色从蓝色渐变到绿色;释放按钮时,动画以相反方式恢复。通过UIView的动画方法和弹簧动画效果,实现平滑自然的过渡。
76 1
|
3月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
【10月更文挑战第18天】本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户按下按钮时,按钮将从圆形变为椭圆形并从蓝色渐变为绿色;释放按钮时,动画恢复原状。通过UIView的动画方法和弹簧动画效果,实现平滑自然的动画过渡。
68 5
|
3月前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,它可能导致应用程序性能下降,甚至使整个系统变得不稳定。
线程死循环是多线程编程中常见的问题,可能导致性能下降或系统不稳定。通过代码审查、静态分析、日志监控、设置超时、使用锁机制、测试、选择线程安全的数据结构、限制线程数、使用现代并发库及培训,可有效预防和解决死循环问题。
101 1