美文网首页
GCD高级功能小结

GCD高级功能小结

作者: Daved | 来源:发表于2017-07-08 20:17 被阅读37次

在介绍完GCD的基本使用后,接下来聊聊GCD的一些高级功能。

dispatch_semaphore(信号量)

信号量是用来管理对资源的并发访问。信号量是持有计数的信号,内部有一个可以原子递增或递减的值。如果有一个操作尝试减少信号量的值,使其小于0,那么该操作将会被阻塞(或等待),直到有其它操作增加该信号量的值(>=1时)。
首先,我们来看下如下这种情况。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    for (int i = 0; i < 100000; i++) {
        dispatch_async(queue, ^{
            [array addObject:[[NSObject alloc] init]];
        });
    }

在不考虑顺序的情况下,将所有数据追加到NSMutableArray中。因为在全局队列中异步更新NSMutableArray对象,执行后程序会因为内存错误而崩溃。
使用dispatch_semaphore对代码进行改造,改造后的代码如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 初始化信号量计数为1,保证可同时访问NSMutableArray对象的线程只有1个
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (int i = 0; i < 100000; i++) {
        dispatch_async(queue, ^{
        
            // 一直等待,直到信号量的计数>=1
            // 信号量计数-1,dispatch_semaphore_wait函数返回
            // 此时信号量计数为0,由于可同时访问NSMutableArray对象的线程只有1个
            // (其它线程此时无法访问NSMutableArray对象),因此可以安全的进行更新
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [array addObject:[[NSObject alloc] init]];
            
            // 处理结束,信号量计数+1,此时其它线程可以访问NSMutableArray对象
            dispatch_semaphore_signal(semaphore);
        });
    }
  • 信号量在实际开发中使用

    • 将异步操作转换为同步操作

      例如:在做请求接口的单元测试时,需要等待响应回调。可以在调用接口后等待信号量,然后在回调里通知该信号量。

          NSURL *URL = [NSURL URLWithString:@"http://xxx.com"];
          __block NSURL *location;
          __block NSError *error;
          // 创建信号量并初始化为0(等待响应结果,阻塞)
          dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
          [[[NSURLSession sharedSession] downloadTaskWithURL:URL
                                           completionHandler:
            ^(NSURL *l, NSURLResponse *r, NSError *e) {
              location = l;
              error = e;
        
              // 响应处理结束,信号量计数+1,解除阻塞
              dispatch_semaphore_signal(semaphore);
            }] resume];
          
          // 设置等待超时时间
          double timeoutInSeconds = 2.0;
          dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW,                          (int64_t)(timeoutInSeconds * NSEC_PER_SEC));
          long timeoutResult = dispatch_semaphore_wait(semaphore, timeout);
          // 断言测试
          XCTAssertEqual(timeoutResult, 0L, @"Timed out");
          XCTAssertNil(error, @"Received an error:%@", error);
          XCTAssertNotNil(location, @"Did not get a location");
      
    • YYModel中信号量的使用

      ```
      NSObject+YYModel.m
      
      + (instancetype)metaWithClass:(Class)cls {
          if (!cls) return nil;
          static CFMutableDictionaryRef cache;
          static dispatch_once_t onceToken;
          static dispatch_semaphore_t lock;
          dispatch_once(&onceToken, ^{
              cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
              lock = dispatch_semaphore_create(1);
          });
          dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
          _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
          dispatch_semaphore_signal(lock);
          if (!meta || meta->_classInfo.needUpdate) {
              meta = [[_YYModelMeta alloc] initWithClass:cls];
              if (meta) {
                  dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
                  CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
                  dispatch_semaphore_signal(lock);
              }
          }
          return meta;
      }
      ```
      
  • 使用信号量注意事项

    • 信号量不依赖GCD调度队列,它可以直接在任何线程中使用
    • 信号量属于底层工具,应该优先考虑使用诸如操作队列这样的高级API
    • 信号量本身是锁,能不用就不用

dispatch_barrier_async

在日常开发中我们经常会遇到这样的情景,需要并发的对数据库进行读、写操作。在多线程环境下并发读、写数据库特别容易产生死锁及其它错误(如:脏读等问题)。

dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read0 task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read1 task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read2 task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read3 task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read4 task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read5 task");
    });

如上所示,在并发读时不会产生死锁等问题。假如:在read2 task之后需要增加一个写入操作,并将其添加到并发队列中,read3以及之后的读取操作需要读取写入的值。

dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
    ...
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read2 task");
    });
    dispatch_async(queue, ^{
        // do some write task
        NSLog(@"write task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read3 task");
    });
    ...

由并发队列的特性可知,以上代码并不会达到预期目的。如果写入操作增多还会导致数据库资源竞争产生死锁,甚至导致应用异常结束。使用dispatch_barrier_async可以帮我们避免此类问题。

dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
    ...
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read2 task");
    });
    dispatch_barrier_async(queue, ^{
        // do some write task
        NSLog(@"write task");
    });
    dispatch_async(queue, ^{
        // do some read task
        NSLog(@"read3 task");
    });
    ...

dispatch_barrier_async会等到追加到并发队列上的read0read2任务处理结束后,再将write任务追加到并发队列中,write任务处理结束后,追加后续read3read5任务以并发方式执行。执行结果如下所示。

2017-xx-xx GCDAdvanceDemo[45482:1778050] read2 task
2017-xx-xx GCDAdvanceDemo[45482:1778038] read0 task
2017-xx-xx GCDAdvanceDemo[45482:1778039] read1 task
2017-xx-xx GCDAdvanceDemo[45482:1778041] write task
2017-xx-xx GCDAdvanceDemo[45482:1778041] read3 task
2017-xx-xx GCDAdvanceDemo[45482:1778039] read4 task
2017-xx-xx GCDAdvanceDemo[45482:1778050] read5 task

dispatch_barrier_async在并发队列中执行流程如下图所示。


dispatch_barrier_async.png

dispatch_source_t(事件源)

事件源可以用来响应并处理系统事件,如:监视进程、文件的变化情况。由于iOS系统的限制,该功能主要用于Mac开发中。

  • 在iOS中一些性能要求较高的场合可以使用自定义源来进行处理进度的反馈,如下所示。
// 自定义事件源,指定累积方式和事件处理队列
dispatch_source_t
  source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD,
                                  0, 0, dispatch_get_main_queue());

  // 事件处理,同一时间只有一个处理块被分派,如果该块尚未处理完成另一事件已发生,
  // 事件会以指定的累积方式DISPATCH_SOURCE_TYPE_DATA_ADD进行累积
  __block long totalComplete = 0;
  dispatch_source_set_event_handler(source, ^{
      // 获取数据,处理后清空
    long value = dispatch_source_get_data(source);
    totalComplete += value;
    self.progressView.progress = (CGFloat)totalComplete/100.0f;
  });
  // 事件源默认处于暂停状态,需要手动恢复
  dispatch_resume(source);

  dispatch_queue_t
  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
                                    0);
  dispatch_async(queue, ^{
    for (int i = 0; i <= 100; ++i) {
        // 向事件源发送数据,数据需要>0否则不会触发事件
      dispatch_source_merge_data(source, 1);
      usleep(20000);
    }
  });
  • 使用DISPATCH_SOURCE_TYPE_TIMER事件源实现定时器
+ (RNTimer *)repeatingTimerWithTimeInterval:(NSTimeInterval)seconds
                                      block:(void (^)(void))block {

  RNTimer *timer = [[self alloc] init];
  timer.block = block;
  // 指定事件源类型
  timer.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                        0, 0,
                                        dispatch_get_main_queue());
  // 定时器参数设置
  uint64_t nsec = (uint64_t)(seconds * NSEC_PER_SEC);
  dispatch_source_set_timer(timer.source,
                            dispatch_time(DISPATCH_TIME_NOW, nsec),
                            nsec, 0);
  // 事件处理block设置
  dispatch_source_set_event_handler(timer.source, block);
  dispatch_resume(timer.source);
  return timer;
}

完整实现请移步RNTimer

dispatch_data、dispatch_io(派发数据、派发IO)

在开发中处理大量IO操作是一件比较有挑战的事情,比如:读取大文件。一种常见的思路是,将文件分割成合适的大小并发读取,如下所示。

    dispatch_async(queue, ^{ /* 读取  0 ~ 8080  字节*/ });  
    dispatch_async(queue, ^{ /* 读取  8081  ~ 16383 字节*/ });  
    dispatch_async(queue, ^{ /* 读取  16384 ~ 24575 字节*/ }); 
    ... 

使用dispatch_data和dispatch_io可以达到上述目的,以下为Apple System Log API的部分代码,展示了如何使用dispatch_data、dispatch_io

     pipe_q = dispatch_queue_create("PipeQ", NULL);  
    // 创建 Dispatch I/O  
    pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){  
        close(fd);  
    });  
    *out_fd = fdpair[1];  
      
    // 设定单次读取数据的大小 
    dispatch_io_set_low_water(pipe_channel, SIZE_MAX);  

    dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){  
        if (err == 0)  
        {  
            // 获取“单个文件块”的大小  
            size_t len = dispatch_data_get_size(pipedata);  
            if (len > 0)  
            {  
                // 定义一个字节数组bytes  
                const charchar *bytes = NULL;  
                charchar *encoded;  
                  
                // 数据处理  
                dispatch_data_t md = dispatch_data_create_map(pipedata, (const voidvoid **)&bytes, &len);  
                encoded = asl_core_encode_buffer(bytes, len);  
                asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);  
                free(encoded);  
                _asl_send_message(NULL, merged_msg, -1, NULL);  
                asl_msg_release(merged_msg);  
                dispatch_release(md);  
            }  
        }  
          
        if (done)  
        {  
            dispatch_semaphore_signal(sem);  
            dispatch_release(pipe_channel);  
            dispatch_release(pipe_q);  
        }  
    });

DispatchDownload展示了如何使用dispatch_io建立和server端的socket通信并通过流的方式下载文件。

dispatch_io属于底层C语言API,在使用时应该优先考虑使用高级API,在高级API无法满足需求的情况下,可以考虑使用底层API以获取更多的控制权。

参考

底层并发API
RNTimer
iOS 7 Programming Pushing the Limits
Objective-C高级编程

相关文章

  • GCD高级功能小结

    在介绍完GCD的基本使用后,接下来聊聊GCD的一些高级功能。 dispatch_semaphore(信号量) 信号...

  • iOS实录16:GCD使用小结(二)

    iOS实录16:GCD使用小结(二) iOS实录16:GCD使用小结(二)

  • GCD基本功能小结

    GCD缘起 GCD(Grand Central Dispatch) 是 Apple 开发的一个基于C的并发编程解决...

  • GCD 对比 NSOprationQueue

    GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。 区别 ...

  • gcd小结

    DCD 其他用法

  • GCD小结

    GCD 全称: Grand Central Dispatch本质: 一套低层级的 C 语言 API,GCD 管理着...

  • GCD 小结

    一、 同步/异步、串行/并行的区别 1.同步/异步 同步/异步是指线程与线程之间的关系. 2.串行/并行 串行/并...

  • GCD小结

    本文总结于 一些 iOS / Web 开发相关的翻译或原创博客文章 GCD (grand_central_disp...

  • GCD学习

    Objective-C高级编程-GCD部分 Grand Central Dispatch(GCD)是异步执行任务的...

  • GCD高级

    http://blog.sina.com.cn/s/blog_626e5d690102uwxx.html

网友评论

      本文标题:GCD高级功能小结

      本文链接:https://www.haomeiwen.com/subject/khyjhxtx.html