iOS 多线程系列 -- GCD全解三(进阶)

iOS 多线程系列 -- 基础概述
iOS 多线程系列 -- pthread
iOS 多线程系列 -- NSThread
iOS 多线程系列 -- GCD全解一(基础)
iOS 多线程系列 -- GCD全解二(常用方法)
iOS 多线程系列 -- GCD全解三(进阶)
iOS 多线程系列 -- NSOperation
测试Demo的GitHub地址

4. GCD进阶:

4.1 Dispatch Source 调度源

4.1.1 Dispatch Source基础介绍
  • 简单来说,dispatch source是一个监视某些类型事件的对象。当这些事件发生时,它会自动将对应的处理程序块(block)提交给调度队列以响应触发事件.

  • 调度源不可重入。事件源被挂起或当事件处理程序块正在执行时接收到的新事件会在源恢复之后或者正在执行的程序块返回后合并/提交.合并机制的出现,是dispatch source为了防止事件积压到dispatch queue.如果新事件在上一个事件处理器出列并执行之前到达,dispatch source会将新旧事件的数据合并。根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息(参考下文Dispatch Source自定义事件打印结果)

  • Dispatch Source有多种类型,不同类型可以实现不同功能,具体类型如下:

    • Timer
      • DISPATCH_SOURCE_TYPE_TIMER : 定时器
    • 自定义的事件,并且也是有自己来触发
      • DISPATCH_SOURCE_TYPE_DATA_ADD : 合并通过调用dispatch_source_merge_data()获得的数据
      • DISPATCH_SOURCE_TYPE_DATA_OR : 同样是合并由dispatch_source_merge_data()获得的数据,不过合并原则是按位OR运算.
      • 相关函数dispatch_source_merge_data()
    • Mach port相关事件响应
      • DISPATCH_SOURCE_TYPE_MACH_SEND : 端口发送
      • DISPATCH_SOURCE_TYPE_MACH_RECV : 端口接收
    • 文件系统监听
      • DISPATCH_SOURCE_TYPE_VNODE : 文件系统有变更
      • DISPATCH_SOURCE_TYPE_WRITE : 可写入文件映像
      • DISPATCH_SOURCE_TYPE_READ : 可读取文件映像
    • 其他
      • DISPATCH_SOURCE_TYPE_SIGNAL : 接收信号
      • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE : 内存压力
      • DISPATCH_SOURCE_TYPE_PROC : 检测到与进程相关的事件
4.1.2 使用Dispatch Source步骤 ,以自定义source事件为例
  • 步骤一: 创建Dispatch Source, 用dispatch_source_create方法

    • 参数一: Dispatch Source的类别,具体类别上面已介绍
    • 参数二/三 : 取决于参数一的配置信息,不同类别的具体配置,可以看头文件详细介绍.
    • 参数四 : Dispatch Source关联的队列
    dispatch_source_create(dispatch_source_type_t type,
        uintptr_t handle,
        unsigned long mask,
        dispatch_queue_t _Nullable queue);
    
    • 创建DISPATCH_SOURCE_TYPE_DATA_ADD类型source
    dispatch_source_t  source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
    
    
  • 步骤二: 设置响应事件,每当监听到事件后就会调用这个响应block

    • 使用dispatch_source_set_event_handler 设置响应事件,参数一是对应的source,参数二是响应的具体代码块
    • dispatch_source_merge_data,向自定义的事件源传递一个unsigned long类型数据,这个数据可以使用dispatch_source_get_data获取到,这样就实现了事件触发到事件响应之间的数据传输
    dispatch_source_set_event_handler(source, ^{
        NSLog(@"%lu 人已报名",dispatch_source_get_data(source));
    });
  • 步骤三:对于一个Dispatch Source还可以设置取消时的对应响应.在这个source被cancel的时候调用.参数一是对应的source,参数二是source被cancel时的响应代码块
dispatch_source_set_cancel_handler(source, ^{
        NSLog(@"报名已终止");
    });

  • 步骤三: 恢复Dispatch Source,新创建的Dispatch Source默认挂起,需要恢复执行
    • 恢复source有两个方法dispatch_resume 和dispatch_activate,任选其一,参数就是要恢复的source. dispatch_activate是iOS10才添加的新方法,按需使用即可.
dispatch_resume(source);
//dispatch_activate(source);

  • 步骤四: 取消一个source

    • dispatch_source_cancel,传入想要取消的source即可. 注意:传入nill会闪退,所以调用之前判断source是否为nil
    if (self.timer) {
            dispatch_source_cancel(self.timer); // self.timer为nil时会闪退
        }
    
    • 如果可能被多线程中调用此方法,还应考虑线程安全,加锁即可.
    @synchronized (self.timer) {        
        if (self.timer) {
            dispatch_source_cancel(self.timer); // timer为null时会闪退
        }
    }
    
  • 示例代码:

dispatch_source_t  source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,dispatch_get_main_queue());
    dispatch_source_set_event_handler(source, ^{
        NSLog(@"%lu 人已报名",dispatch_source_get_data(source));
    });
    dispatch_resume(source);
    dispatch_apply(5, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSLog(@"用户%zd 报名郊游",index);
        dispatch_source_merge_data(source, 1); // 触发事件,传递数据
    });
  • 打印结果
2017-07-01 18:34:30.514 Test - 多线程[66363:2522266] 用户2 报名郊游
2017-07-01 18:34:30.513 Test - 多线程[66363:2522111] 用户0 报名郊游
2017-07-01 18:34:30.513 Test - 多线程[66363:2522265] 用户1 报名郊游
2017-07-01 18:34:30.514 Test - 多线程[66363:2522359] 用户3 报名郊游
2017-07-01 18:34:30.514 Test - 多线程[66363:2522266] 用户4 报名郊游
2017-07-01 18:34:30.514 Test - 多线程[66363:2522111] 5 人已报名

  • Dispatch Source进阶分析
    • 细心的同学会发现上面的打印结果很有意思,有五个用户报名,结果统计人数打印只有一次,为什么不是五次?难道不应该报名一次统计一次嘛?这就是为什么使用Dispatch Source的原因所在,分析如下:
    • 主线程只不过是GCD的另一个dispatch queue,我们把大量的响应工作push到主线程中,主线程每一次都处理响应事件可能会消耗很多资源,我们不想对响应工作进行频繁而累赘的更新,理想的情况是当主线程繁忙时将所有的响应工作联结起来。这时用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我们可以将工作拼接起来,然后主线程可以知道从上一次处理完事件到现在一共发生了多少改变,然后将这一整段改变一次更新至最新进度.所以从打印结果可以看到,系统把五次响应联结起来,组成了一次任务输出,这在节省性能方面很有帮助!
4.1.3 使用Dispatch Source自定义定时器

在上面提到的几个步骤的基础上,还需要使用,dispatch_source_set_timer设置定时器属性,当然创建source的类型要选择DISPATCH_SOURCE_TYPE_TIMER

  • dispatch_source_set_timer方法这是定时器属性,参数解析:
    • 第一个参数:dispatch_source_t 创建的定时器类型source
    • 第二个参数:dispatch_time_t start, 定时器开始时间,类型为 dispatch_time_t,dispatch_time_t有两个创建方法,分别是:dispatch_time和dispatch_walltime,区别介绍如下:
      • 当我们设置为dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止
      • 同为设置时间,但是dispatch_w,alltime为“钟表”时间,相对比较准确,所以选择使用后者。dispatch_walltime有两个参数
        • 参数when可以为Null/DISPATCH_TIME_NOW,默认为获取当前时间;
        • 参数delta为增量,注意delta的单位是纳秒.即获取当前时间的基础上,增加delta纳秒的时间为开始计时时间.如果想要延迟1秒的话,那就是1000000000这样写太长了,系统理所当然的提供了对应的常量宏.关键字解释:NSEC纳秒,PER每,SEC秒,MSEC毫秒,USEC微秒.所以NSEC_PER_SEC的意思是每秒有多少纳秒,那么延迟1秒就可以写成1* NSEC_PER_SEC
      • 介绍两个常用dispatch_time_t宏,ull表示unsigned long long类型,~表示按位取反. 所以就能理解DISPATCH_TIME_NOW = 0, DISPATCH_TIME_FOREVER至少是一个不小于2^64 - 1的数值
    • 第三个参数:uint64_t interval,定时器间隔时长,由业务需求而定。
    • 第四个参数:uint64_t leeway, 允许误差,此处传0即可,需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
        dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
    dispatch_time_t dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);

            #define NSEC_PER_SEC 1000000000ull
            #define NSEC_PER_MSEC 1000000ull
            #define USEC_PER_SEC 1000000ull
            #define NSEC_PER_USEC 1000ull
        #define DISPATCH_TIME_NOW (0ull)
        #define DISPATCH_TIME_FOREVER (~0ull)

  • GCD定时器示例代码:

- (void)dispatchTimer
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    _timer = timer;
    //NSEC_PER_SEC宏其定义是1000000000纳秒,也就是1秒
    dispatch_source_set_timer(timer, dispatch_walltime(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC), 1ull * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"-----handle dispatchTimer thread = %@",[NSThread currentThread]);
    });
    dispatch_source_set_cancel_handler(timer, ^{
        NSLog(@"-----cancel dispatchTimer thread = %@",[NSThread currentThread]);
    });
    dispatch_resume(timer);//启动定时器
}
4.1.4 Dispatch Source补充
  • dispatch_source_testcancel,查看source是否已经取消,返回0表示没有取消,返回值非0表示已经取消
  • 参考资料:
  • gcd介绍(三)-dispatch-sources
  • GCD dispatch source](http://blog.csdn.net/pingshw/article/details/16940009))

4.2 dispatch semaphore

信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知(dispatch_semaphore_signal)和等待(dispatch_semaphore_wait).

4.2.1 操作dispatch semaphore的三个方法
  • dispatch_semaphore_create   创建一个semaphore,有一个long类型的参数value,表示这个信号量的初始值,注意: value的值不能小于0,否则创建失败返回NULL.
dispatch_semaphore_t dispatch_semaphore_create(long value);
  • dispatch_semaphore_wait   让信号值减1,等待信号.参数dsema表示要操作的信号量, timeout为等待时间.让信号值减1后这个方法有两种处理操作:
    • 如果信号值减1后值小于0,阻塞线程直到信号值不小于0为止,什么时候线程被唤醒是在dispatch_semaphore_signal方法中处理的
    • 如果信号值减1后值大于等于0,继续往下执行,不阻塞线程
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
  • dispatch_semaphore_signal   让信号值加1,发送一个信号,参数dsema表示信号值要加1的信号量.如果信号量在加1之前小于0, 会唤醒这个被阻塞的线程.唤醒成功返回一个非零的值,否则返回0.
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

4.2.2 进阶 - dispatch semaphore的实用场景

上面已经介绍dispatch semaphore的基本使用方法,那利用dispatch semaphore可以实现那些功能?

  • dispatch semaphore实现GCD线程并发控制

    • NSOperationQueue中可以利用maxConcurrentOperationCount设置最大并发数,GCD中如何实现最大并发数量的控制呢? 我们可以利用dispatch semaphore实现GCD线程并发控制,如下代码: 调度组中最多有3个queue任务在执行,只有执行完一个才能添加另一个到调度组,进而实现并发控制
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_t group = dispatch_group_create();
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);        
        for (int i = 0; i < 100; i++)
        {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            dispatch_group_async(group, queue, ^{
                sleep(5);
                NSLog(@"结束任务i = %d , thread = %@",i,[NSThread currentThread]);
                dispatch_semaphore_signal(semaphore);
            });
        }
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    }); 
    
  • 控制共有资源的多线程访问数据安全

    • 如果多个线程同时操纵一份共有资源,会引发数据错误的问题.使用dispatch_semaphore_create(1) 创建一个同一时间只允许一个线程操纵数据的信号量即可保证数据的安全.其实有点类似于线机制,一种线程同步技术.
    • 示例代码,保证数组内用户插入顺序如下:
    // 保证用户的插入顺序
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    NSMutableArray *users = [NSMutableArray array];
    for (int i = 0; i < 300; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
             // 访问共有资源的任务代码
            NSLog(@"i = %zd , 添加一个用户,thread = %@",i,[NSThread currentThread]);
            [users addObject:@(i)];
            dispatch_semaphore_signal(semaphore);
        });
    }
    

你可能感兴趣的:(iOS 多线程系列 -- GCD全解三(进阶))