Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法


Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法

【前言】从iOS7升到iOS8后,GCD 出现了一个重大的变化:在 iOS7 时,使用 GCD 的并行队列, dispatch_async 最大开启的线程一直能控制在6、7条,线程数都是个位数,然而 iOS8后,最大线程数一度可以达到40条、50条。然而在文档上并没有对这一做法的目的进行介绍。

笔者推测 Apple 的目的是想借此让开发者使用 NSOperationQueue :GCD 中 Apple 并没有提供控制并发数量的接口,而NSOperationQueue 有。GCD 没有提供暂停、恢复、取消队列任务的接口,而 NSOperationQueue 有,如果想让 GCD 支持 NSOperationQueue 原生就支持的功能,需要使用许多GCD 的高级功能,大大提高了使用的难度。

Apple 始终有一个观念:尽可能选用高层 API,只在确有必要时才求助于底层。然而开发者并不买账,在我进行的一次调查 中发现了一个有趣的现象:

大概 80%的iOS 开发者会支持使用 GCD 来完成操作队列的实现,而且有 60% 的开发已经在项目中使用。

Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法_第1张图片

更是有人这样表态:

假如不让他用 GCD:

Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法_第2张图片

这种现象一直存在,包括 ARC 与 MRC、SB建 UI 与纯代码建 UI、SQL 与 CoreData的争论。

但是因为是源码解析的文章,而 Parse 的 SDK 没有用一句的 NSOperation 的代码,GCD 一路用到底,让我也十分震惊。只能说明,写 Parse 的这位开发者是艺高人胆大。而且既然 GCD 的支持者如此之多,那么就谈一谈如何让 GCD 能支持NSOperationQueue 原生就支持的功能。

今天虽然谈了NSOperation原生功能的 GCD 版本实现,但并不代表我支持像 Parse 这样 GCD 一路用到底。 业内一般的看法是这样的:

GCD 虽然能够实现暂停和终止,但开发还是灵活些好,那些 NSOperation 用起来方便的就直接用 NSOperation 的方式,不然苹果多包那一层不是蛋疼,包括文章里提到的 iOS8 后控制线程数的问题,不一定项目就一定要GCD一路到底。有时候需要支持一些高层级封装功能比如: KVO 时 NSOperation 还是有它的优势的。 GCD 反而是处理些比较简单的操作或者是较系统级的比如:监视进程或者监视文件夹内文件的变化之类的比较合适。

(iOS开发学习交流群:512437027)

第一篇的目的是通过解读 Parse 源码来展示GCD两个高级用法: Dispatch Source (派发源)和 Dispatch Semaphore(信号量)。首先通过Parse 的“离线存储对象”操作,来介绍 Dispatch Source (派发源);然后通过Parse 的单元测试中使用的技巧“强制把异步任务转换为同步任务来方便进行单元测试”来介绍Dispatch Semaphore (信号量)。我已将思路浓缩为可运行的7个 Demo 中,详见仓库里的 Demo1到 Demo7。

如果对 GCD 不太熟悉,请先读下《GCD 扫盲篇》。

  1. Dispatch Source分派源

    1. Parse-iOS-SDK介绍

    2. Parse 的“离线存储对象”操作介绍

    3. Parse 的“离线存储对象”实现介绍
    4. Dispatch Source 的使用步骤
      1. 第一步:创建一个Dispatch Source
      2. 第二步:创建Dispatch Source的事件处理方法
      3. 第三步:处理Dispatch Source的暂停与恢复操作
      4. 第四步:向Dispatch Source发送事件
    5. GCD真的不能像OperationQueue那样终止任务?
    6. 完整例子Demo1:让 Dispatch Source “帮” Dispatch Queue 实现暂停和恢复功能
    7. DispatchSource能通过合并事件的方式确保在高负载下正常工作
    8. Dispatch Source 与 Dispatch Queue 两者在线程执行上的关系
    9. 让 Dispatch Source 与 Dispatch Queue 同时实现暂停和恢复
    10. Parse “离线存储对象”操作的代码摘录
  2. Dispatch Semaphore 信号量
    1. 在项目中的应用:强制把异步任务转换为同步任务来方便进行单元测试
    2. 使用Dispatch Semaphore控制并发线程数量

Parse-iOS-SDK介绍

《iOS开发周报:iOS 8.4.1 发布,iOS 8 时代谢幕》 对 Facebook 旗下的 Parse有这样一段介绍:

Parse-SDK-iOS-OSX:著名的 BaaS 公司 Parse 最近开源了它们的 iOS/OSX SDK。Parse 的服务虽然在国内可能访问速度不是很理想,但是它们在服务的稳定性和 SDK 质量上一直有非常优异的表现。此次开源的 SDK 对于日常工作是 SDK 开发的开发者来说,是一个难得的学习机会。Parse 的存取操作涉及到很多多线程的问题,从 Parse SDK 的源代码中可以看出,这个 SDK 的开发者对 iOS 开发多线程有着非常深厚的理解和功底,让人叹服。我个人推荐对此感兴趣的朋友可以尝试从阅读 internal 文件夹下的两个EventuallyQueue 文件开始着手,研究下 Parse 的底层多线程处理思路。

类似的服务: Apple 的 Cloud​Kit 、 国内的 LeanCloud(原名 AVOS ) 。

Parse 的“离线存储对象”操作介绍

大多数保存功能可以立刻执行,并通知应用“保存完毕”。不过若不需要知道保存完成的时间,则可使用“离线存储对象”操作(saveEventually 或 deleteEventually) 来代替,也就是:

如果用户目前尚未接入网络,“离线存储对象”操作(saveEventually 或 deleteEventually) 会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会自动再次尝试保存操作。

所有 saveEventually(或 deleteEventually)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 saveEventually 是安全的。

国内的 LeanCloud(原名 AVOS ) 也提供了相同的功能,所以以上《Parse 的“离线存储对象”操作介绍》部分完全摘录自 LeanCloud 的文档。详见《LeanCloud官方文档-iOS / OS X 数据存储开发指南--离线存储对象》

(利益相关声明:本人目前就职于 LeanCloud(原名 AVOS ) )

Parse 的“离线存储对象”实现介绍

Parse 的“离线存储对象”操作(saveEventually 或 deleteEventually) 是通过 GCD 的 Dispatch Source (信号源)来实现的。下面对 Dispatch Source (信号源)进行一下介绍:

GCD中除了主要的 Dispatch Queue 外,还有不太引人注目的 Dispatch Source .它是BSD系内核惯有功能kqueue的包装。kqueue 是在 XNU 内核中发生各种事件时,在应用程序编程方执行处理的技术。其 CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理 XNU 内核中发生的各种事件的方法中最优秀的一种。

Dispatch Source 也使用在了 Core Foundation 框架的用于异步网络的API CFSocket 中。因为Foundation 框架的异步网络 API 是通过CFSocket实现的,所以可享受到仅使用 Foundation 框架的 Dispatch Source 带来的好处。

那么优势何在?使用的 Dispatch Source 而不使用 dispatch_async 的唯一原因就是利用联结的优势。

联结的大致流程:在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。

这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件。

简单地说,这种事件是由你调用 dispatch_source_merge_data 函数来向自己发出的信号。

下面介绍下使用步骤:

Dispatch Source 的使用步骤

第一步:创建一个Dispatch Source

    // 详见 Demo1、Demo2
    // 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
    _processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
                                                    dispatch_get_main_queue());

下面对参数进行下解释:

其中自定义源累积事件中传递过来的值,累积的方式可以是相加的,正如上面代码中的DISPATCH_SOURCE_TYPE_DATA_ADD ,也可以是逻辑或 DISPATCH_SOURCE_TYPE_DATA_OR 。这是最常见的两个 Dispatch Source 可以处理的事件。

Dispatch Source 可处理的所有事件。如下表所示:

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 监测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

自定义源也需要一个队列,用来处理所有的响应句柄(block)。那么岂不是有两个队列了?没错,至于 Dispatch Queue这个队列的线程执行与 Dispatch Source这个队列的线程执行的关系,下文会结合 Demo1和 Demo2进行详细论述。

第二步:创建Dispatch Source的事件处理方法

分派源提供了高效的方式来处理事件。首先注册事件处理程序,事件发生时会收到通知。如果在系统还没有来得及通知你之前事件就发生了多次,那么这些事件会被合并为一个事件。这对于底层的高性能代码很有用,但是OS应用开发者很少会用到这样的功能。类似地,分派源可以响应UNIX信号、文件系统的变化、其他进程的变化以及Mach Port事件。它们中很多都在Mac系统上很有用,但是iOS开发者通常不会用到。

不过,自定义源在iOS中很有用,尤其是在性能至关重要的场合进行进度反馈。如下所示,首先创建一个源:自定义源累积事件中传递过来的值。累积方式可以是相加( DISPATCH_SOURCE_TYPE_DATA_ADD ), 也可以是逻辑或( DISPATCH_SOURCE_DATA_OR )。自定义源也需要一个队列,用来处理所有的响应处理块。

创建源后,需要提供相应的处理方法。当源生效时会分派注册处理方法;当事件发生时会分派事件处理方法;当源被取消时会分派取消处理方法。自定义源通常只需要一个事件处理方法,可以像这样创建:

 /*
  *省略部分: 
    指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
    详见Demo1、Demo2
  *
  */
    __block NSUInteger totalComplete = 0;
    dispatch_source_set_event_handler(_processingQueueSource, ^{
        //当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
        NSUInteger value = dispatch_source_get_data(_processingQueueSource);
        totalComplete += value;
        NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
    });

在同一时间,只有一个处理方法块的实例被分派。如果这个处理方法还没有执行完毕,另一个事件就发生了,事件会以指定方式(ADD或者OR)进行累积。通过合并事件的方式,系统即使在高负 载情况下也能正常工作。当处理事件件被最终执行时,计算后的数据可以通过 dispatch_source_get_data 来获取。这个数据的值在每次响应事件执行后会被重置,所以上面例子中 totalComplete 的值是最终累积的值。

第三步:处理Dispatch Source的暂停与恢复操作

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。

在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。

dispatch_suspend(queue);

dispatch_resume 函数恢复指定的 Dispatch Queue . 这些函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。因为忘记恢复分派源的状态而产生bug是常见的事儿。恢复的方法是调用 dispatch_resume :

dispatch_resume (source);

第四步:向Dispatch Source发送事件

恢复源后,就可以像下面的代码片段这样,通过 dispatch_source_merge_data 向分派源发送事件:

    //2.
    //恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
    //详见Demo1、Demo2
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        for (NSUInteger index = 0; index < 100; index++) {
            dispatch_async(queue, ^{
            dispatch_source_merge_data(_processingQueueSource, 1);
            usleep(20000);//0.02秒
            });
        }

上面代码在每次循环中执行加1操作。也可以传递已处理记录的数目或已写入的字节数。在任何线程中都可以调用dispatch_source_merge_data 。需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。

GCD真的不能像OperationQueue那样终止任务?

完整例子Demo1:让 Dispatch Source “帮” Dispatch Queue 实现暂停和恢复功能

本节配套代码在 Demo1 中(Demo_01_对DispatchSource实现取消恢复操作_main队列版)。

先写一段代码演示下DispatchSource的基本用法:

//
//  .m
//  CYLDispatchSourceTest
//
//  Created by 微博@iOS程序犭袁( http://weibo.com/luohanchenyilong/) on 15/9/1.
//  Copyright (c) 2015年 https://github.com/ChenYilong . All rights reserved.
//

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.
    // 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
    _processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
                                                    dispatch_get_main_queue());
    __block NSUInteger totalComplete = 0;
    dispatch_source_set_event_handler(_processingQueueSource, ^{
        //当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
        NSUInteger value = dispatch_source_get_data(_processingQueueSource);
        totalComplete += value;
        NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
        NSLog(@"

你可能感兴趣的:(Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法)