GCD高级功能小结

在介绍完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高级编程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容