第45条:使用 dispatch_once 来执行只需运行一次的线程安全代码

  本条要点:(作者总结)

  • 经常需要编写 “只需执行一次的线程安全代码”(thread-safe single-code execution)。通过 GCD 所提供的 dispatch_once 函数,很容易就能实现此功能。
  • 标记应该声明在 statci 或 global 作用域中,这样的话,在把只需执行一次的块传给dispatch_once 函数时,传进去的标记也是相同的。

  单例模式(singleton)对 Objective-C 开发者来说并不陌生,常见的实现方式为:在类中编写名为 sharedInstance  的方法,该方法只会返回全类共用的单例实例,而不会在每次调用时都创建新的实例。假设有个类叫做 EOCClass,那么这个共享实例的方法一般都会这样写:

 1   @implementation EOCClass
 2 
 3   + (id)sharedInstance {
 4 
 5     static EOCClass *sharedInstance = nil;
 6     @synchronized(self) {
 7       if (!sharedInstance) {
 8         sharedInstance = [ [self alloc] init];
 9       }
10 
11     }
12     return sharedInstance;
13   }
14 
15   @end

  笔者发现单例模式容易引起激烈争论,Objective-C 的单例尤其如此。线程安全是大家争论的主要问题。为保证线程安全,上述代码将创建单例实例的代码包裹在同步块里。

  无论是好是坏,反正这种实现方式很常用,这样的代码随处可见。

  不过,GCD 引入了一项特性,能使单例实现起来更为容易。所用的函数是:

1 void dispatch_once(dispatch_once_t *token, dispatch_block_t block);

   此函数接受类型为 dispatch_once_t 的特殊参数,笔者称其为 “标记”(token),此外还接受块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。首次调用该函数时,必然会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。请注意,对于只需执行一次的块来说,每次调用函数时传入的标记都必须完全相同。因此,开发者通常将标记变量声明在 static 或 global 作用域里。

  刚才实现单例模式所用的 sharedInstance 方法,可以用此函数来改写:

 1   + (id)sharedInstance {
 2 
 3     static EOCClass *sharedInstance = nil;
 4     static dispatch_once_t onceToken;
 5     dispatch_once(&onceToken, ^{
 6 
 7       sharedInstance = [[self alloc] init];      
 8 
 9     });
10     return sharedInstance;
11   }

  使用 dispatch_once 可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由 GCD 在底层处理。由于每次调用时都必须使用完全相同的标记,所以标记要声明成 static。把该变量定义在 static 作用域中,可以保证编译器在每次执行 sharedInstance 方法时都会复用这个变量,而不会创建新变量。

  此外,dispatch_once 更高效。它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,相反,此函数采用“原子访问”(atomic access)来查询标记,以判断其所对应的代码原来是否已经执行过。笔者在自己装有 64 位 Mac OS X 10.8.2 系统的电脑上简单测试了性能,分别采用 @synchronized 方式及 dispatch_once 方式来实现 sharedInstance 方法,结果显示,后者的速度几乎是前者的两倍。

  END

posted @ 2017-08-24 00:36  鳄鱼不怕牙医不怕  阅读(753)  评论(0编辑  收藏  举报