《Objective-C高级编程》三篇总结之三:GCD篇

Grand Central Dispatch 是这本书的最后一章,作者先从 CPU 多核命令执行简述多线程编程的概念,然后开始逐个分析 GCD 中常用的 API,到最后底层分析 GCD 的源码实现。这里参考 《Objective-C 高级编程》干货三部曲(三):GCD篇,来对这最后一个篇章的内容作总结学习。

Objective-C高级编程.jpg

写在前面

书中描述的,开发者要自己使用函数 dispatch_retaindispatch_release 手动管理队列的内存,是在低于 iOS 6.0 or Mac OS X 10.8 以下的系统中需要做的事情。在 iOS 6.0 or Mac OS X 10.8 及更高版本中,GCD 对象已经纳入 ARC 的管理,所以这两个函数也不再用了。感兴趣可搜索宏 OS_OBJECT_USE_OBJC 查看更多相关信息。

多线程编程

多线程编程实际上是一种易发生各种问题的编程技术:

  • 数据竞争:多个线程更新相同的资源会导致信息不一致。
  • 死锁:多个线程相互持续等待。
  • 太多线程会消耗大量内存。

虽然使用多线程编程会使程序复杂化,但是仍然是必须的:因为使用多线程编程可保证应用程序的响应性能

如果耗时操作阻塞了主线程的 RunLoop,会导致用户界面不能更新、应用程序的画面长时间停滞等问题,所以需要将耗时操作放到子线程中处理。

队列 Dispatch Queue

苹果官方文档对 GCD 的说明:开发者要做的只是定义想要执行的任务并追加到适当的 Dispatch Queue 中

Dispatch Queue 即是执行处理的等待队列,其按照先进先出 FIFO 的顺序执行添加的任务。在执行处理时存在以下两种 Dispatch Queue:

  • Serial Dispatch Queue,串行队列,即等待现在正在执行的任务处理结束,然后在处理下一个。
  • Concurrent Dispatch Queue,并发队列,即不等待现在正在执行的任务处理结束,并开始下一个任务。

创建队列 dispatch_queue_create

通过 dispatch_queue_create 函数可以创建队列,第一个参数是队列名,第二参数,是 NULLDISPATCH_QUEUE_SERIAL 时,返回串行队列,如果是 DISPATCH_QUEUE_CONCURRENT,则返回并发队列。

还可以自己通过函数 dispatch_queue_attr_make_with_ 来自定义队列参数,不过很少使用。

另外,在低于 iOS 6.0 or Mac OS X 10.8 以下的系统中,需要开发者自己通过 dispatch_retaindispatch_release 函数管理 GCD 的内存。在 iOS 6.0 or Mac OS X 10.8 及更高版本中,GCD 对象已经纳入 ARC 的管理,所以这两个函数也不再用了

串行队列 Serial Dispatch Queue

将任务追加到穿行队列:

/// 串行队列
-(void)serialQueue {
    dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
    for (int i = 0; i < 6; i++) {
        dispatch_async(serialQ, ^{
            NSLog(@"task index in serial queue: %d     thread: %@", i, [NSThread currentThread]);
        });
    }
}

输出:

2019-12-12 17:26:33.748110+0800 GCD[25621:1562739] task index in serial queue: 0     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748335+0800 GCD[25621:1562739] task index in serial queue: 1     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748498+0800 GCD[25621:1562739] task index in serial queue: 2     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748642+0800 GCD[25621:1562739] task index in serial queue: 3     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748775+0800 GCD[25621:1562739] task index in serial queue: 4     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748935+0800 GCD[25621:1562739] task index in serial queue: 5     thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}

对于串行队列,都是以阻塞的形式顺序执行的,这里只是做个验证。

一旦生成 Serial Dispatch Queue 并追加处理,系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程。如果生成2000个 Serial Dispatch Queue 并追加处理,那么就生成2000个线程。可是过多线程会消耗大量内存,引起大量的上下文切换,大幅度降低系统响应性能 ,所以我们应该只在必要时考虑使用 Serial Dispatch Queue,譬如多个线程更新相同资源导致数据竞争时。

如果我们将6个任务追加到6个 Serial Dispatch Queue 中,那么系统就会同时处理这6个任务,创建6个子线程:

/// 串行队列
-(void)serialQueue {
    for (int i = 0; i < 6; i++) {
        dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
        dispatch_async(serialQ, ^{
            NSLog(@"task index in serial queue: %d     thread: %@", i, [NSThread currentThread]);
        });
    }
}

输出:

2019-12-12 17:45:50.996286+0800 GCD[26487:1627877] task index in serial queue: 4     thread: <NSThread: 0x600002cf7980>{number = 7, name = (null)}
2019-12-12 17:45:50.996286+0800 GCD[26487:1627881] task index in serial queue: 1     thread: <NSThread: 0x600002cca7c0>{number = 6, name = (null)}
2019-12-12 17:45:50.996286+0800 GCD[26487:1627884] task index in serial queue: 5     thread: <NSThread: 0x600002ce1500>{number = 5, name = (null)}
2019-12-12 17:45:50.996332+0800 GCD[26487:1627878] task index in serial queue: 3     thread: <NSThread: 0x600002cfb080>{number = 3, name = (null)}
2019-12-12 17:45:50.996343+0800 GCD[26487:1627894] task index in serial queue: 2     thread: <NSThread: 0x600002cf7a40>{number = 8, name = (null)}
2019-12-12 17:45:50.996363+0800 GCD[26487:1627880] task index in serial queue: 0     thread: <NSThread: 0x600002cf01c0>{number = 4, name = (null)}

从结果来看,这里的6个任务并不是顺序执行的。并且创建了6个线程来处理。

并发队列 Concurrent Dispatch Queue

iOS 和 MacOS 的核心--XNU 内核决定应当使用的线程数,并只生成所需要的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU 内核会结束不再需要的线程。XNU 内核仅使用 Concurrent Dispatch Queue 便可完美地管理并执行多个处理的线程。

下面将任务追加到并发队列:

/// 并发队列
-(void)serialQueue {
   dispatch_queue_t concurrentQ = dispatch_queue_create("sconcurrent queue", DISPATCH_QUEUE_CONCURRENT);
   for (int i = 0; i < 6; i++) {
       dispatch_async(concurrentQ, ^{
           NSLog(@"task index in serial queue: %d     thread: %@", i, [NSThread currentThread]);
       });
   }
}

输出:

2019-12-12 17:44:01.782371+0800 GCD[26399:1622122] task index in serial queue: 0     thread: <NSThread: 0x60000070bdc0>{number = 5, name = (null)}
2019-12-12 17:44:01.782437+0800 GCD[26399:1622119] task index in serial queue: 1     thread: <NSThread: 0x600000758d40>{number = 4, name = (null)}
2019-12-12 17:44:01.782575+0800 GCD[26399:1622119] task index in serial queue: 4     thread: <NSThread: 0x600000758d40>{number = 4, name = (null)}
2019-12-12 17:44:01.782579+0800 GCD[26399:1622122] task index in serial queue: 5     thread: <NSThread: 0x60000070bdc0>{number = 5, name = (null)}
2019-12-12 17:44:01.782623+0800 GCD[26399:1622117] task index in serial queue: 3     thread: <NSThread: 0x600000764a80>{number = 6, name = (null)}
2019-12-12 17:44:01.782609+0800 GCD[26399:1622118] task index in serial queue: 2     thread: <NSThread: 0x600000741840>{number = 3, name = (null)}

系统提供的队列

Main Dispatch Queue

在主线程中执行的 Dispatch Queue。因为主线程只有一个,所以 Main Dispatch Queue 自然就是 Serial Dispatch Queue。追加到 Main Dispatch Queue 的任务在主线程的 RunLoop 中执行。

获取方法:

// Main Dispatch Queue 的获取方法
dispatch_queue_t mianQueue = dispatch_get_main_queue();
Global Dispatch Queue

Global Dispatch Queue 是所有引用程序都能够使用的 Concurrent Dispatch Queue。我们没有必要通过 dispatch_queue_create 函数逐个生成 Concurrent Dispatch Queue,只要获取 Global Dispatch Queue 使用即可。

Global Dispatch Queue 有四个优先等级:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

获取方式如下:

// Global Dispatch Queue 高优先级 获取方法
dispatch_queue_t highGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

// Global Dispatch Queue 默认优先级 获取方法
dispatch_queue_t defaultGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// Global Dispatch Queue 低优先级 获取方法
dispatch_queue_t lowGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

// Global Dispatch Queue 后台优先级 获取方法
dispatch_queue_t backgroundGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

其中第二个参数 flag 默认为0,官方解释为 Reserved for future use. Passing any value other than zero may result in a NULL return value.。为未来预留的字段,如果不设为 0, 就可能返回一个空的队列。

举个简单的例子来说明二者的使用场景:

//获取全局并发队列进行耗时操作 
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

          //加载图片
          NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
          UIImage *imageFromData = [UIImage imageWithData:dataFromURL];

      dispatch_async(dispatch_get_main_queue(), ^{

              //获取主队列,在图片加载完成后更新UIImageView
              UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];          
      });      
  });

GCD 的 API 们

dispatch_set_target_queue

这个函数有两个作用:

  • 改变队列的优先级
  • 阻止多个串行队列的并发执行。注意,是 多个 串行队列,不是一个串行队列下的多个任务。
改变队列的优先级

首先要知道一点,dispatch_queue_create 函数生成的 Dispatch Queue 不论是 Serial Dispatch Queue,还是 Concurrent Dispatch Queue,都使用和 Global Dispatch Queue 默认优先级相同等级的线程。即 DISPATCH_QUEUE_PRIORITY_DEFAULT。

如果想要改变生成队列的优先级,就需要使用 dispatch_set_target_queue 函数,如下:

/// 改变队列优先级
-(void)changePriority {
    dispatch_queue_t defaultQueue = dispatch_queue_create("serial queue", NULL);
    dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    // 第一个参数:需要改变优先级的队列
    // 第二个参数:目标队列,即作为参照物的队列。
    dispatch_set_target_queue(defaultQueue, bgQueue);
        
    // 此时,defaultQueue 的执行优先级为 DISPATCH_QUEUE_PRIORITY_BACKGROUND
}

注意,第一个参数必须是通过 dispatch_queue_create 函数生成的 Dispatch Queue,如果使用系统提供的 Main Dispatch Queue 和 Global Dispatch Queue 则不知道会出现什么状况,因为系统提供的队列不可更改优先级。

阻止多个串行队列的并发执行

有时我们希望多个 Serial Dispatch Queue 可以顺序执行,便可以通过 dispatch_set_target_queue 函数来做执行约束。如下:

/// 多个 Serial Dispatch Queue 并发执行
-(void)someSerialQueue {
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < 5; i++) {
        // 创建 5 个串行队列
        dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
        [array addObject:serialQ];
    }
    
    // 并发执行
    [array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
        dispatch_async(queue, ^{
            NSLog(@"任务ID: %ld", idx);
        });
    }];
}

输出:

2019-12-11 15:21:02.999308+0800 GCD[57283:11864893] 任务ID: 2
2019-12-11 15:21:02.999310+0800 GCD[57283:11864896] 任务ID: 4
2019-12-11 15:21:02.999353+0800 GCD[57283:11864889] 任务ID: 1
2019-12-11 15:21:02.999379+0800 GCD[57283:11864892] 任务ID: 3
2019-12-11 15:21:02.999497+0800 GCD[57283:11864891] 任务ID: 0

可以看到,当我使用 dispatch_async 异步执行这些任务时,他们执行顺序是不固定的。

通过 dispatch_set_target_queue 可以阻止这种并发现象:

-(void)someSerialQueue {
    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t targetQueue = dispatch_queue_create("target queue", NULL);
    for (int i = 0; i < 5; i++) {
        // 创建 5 个串行队列
        dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
        dispatch_set_target_queue(serialQ, targetQueue);
        [array addObject:serialQ];
    }
    
    // 并发执行
    [array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
        dispatch_async(queue, ^{
            NSLog(@"任务ID: %ld", idx);
        });
    }];
}

输出:

2019-12-11 15:27:38.262131+0800 GCD[57724:11885714] 任务ID: 0
2019-12-11 15:27:38.262377+0800 GCD[57724:11885714] 任务ID: 1
2019-12-11 15:27:38.262551+0800 GCD[57724:11885714] 任务ID: 2
2019-12-11 15:27:38.262690+0800 GCD[57724:11885714] 任务ID: 3
2019-12-11 15:27:38.262838+0800 GCD[57724:11885714] 任务ID: 4

其实,即使不使用 dispatch_set_target_queue,而是将 dispatch_async 修改为 dispatch_sync 或者直接去掉 dispatch_async ,直接 NSLog 输出,都可以顺序打印,这里只是举这么例子证明这个函数有此功能。

dispatch_after

dispatch_after 解决的问题:在某个线程,在指定时间段后,执行某个任务。如下:

-(void)dispatchAfter {
    dispatch_after(DISPATCH_TIME_NOW + 3 * NSEC_PER_SEC, dispatch_get_main_queue(), ^{
        NSLog(@"3 秒后将 NSLog 事件追加到主线程中");
    });
}

注意:dispatch_after 函数并不是在指定时间执行任务处理,而是在指定时间追加处理到 Dispatch Queue。上面代码,与 3 秒后,用 dispatch_async 函数追加 Block 到 Main Dispatch Queue 相同。

因为 Main Dispatch Queue 在主线程的 RunLoop 中执行,所以在每隔 1/60 秒执行的 RunLoop 中,Block 最快在 3 秒后执行,最慢在 3 秒 +1/60 秒后执行,并且 Main Dispatch Queue 有大量处理追加,或者主线程的处理本身有延迟时,这个时间会更长。

dispatch_group

如果遇到一个需求:在全部任务处理完毕后,执行某个操作。如果只使用一个 Serial Dispatch Queue,只需要将想要执行的处理全部追加到后面即可。但是实际开发中,这种方式很容易造成线程的阻塞,所以大都还是需要使用 dispatch_group 来实现的。譬如某个页面有3个接口数据组成,拿到三个接口数据后刷新界面这样的简单需求。

-(void)dispatchGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0; i < 5; i++) {
        dispatch_group_async(group, queue, ^{
            NSLog(@"任务ID: %d", i);
        });
    }
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"任务执行完毕了");
    });
}

输出:

2019-12-11 15:59:22.567149+0800 GCD[59633:11996591] 任务ID: 0
2019-12-11 15:59:22.567161+0800 GCD[59633:11996593] 任务ID: 2
2019-12-11 15:59:22.567149+0800 GCD[59633:11996594] 任务ID: 1
2019-12-11 15:59:22.567277+0800 GCD[59633:11996598] 任务ID: 3
2019-12-11 15:59:22.567406+0800 GCD[59633:11996593] 任务ID: 4
2019-12-11 15:59:22.567543+0800 GCD[59633:11996593] 任务执行完毕了

dispatch_group_notify 函数的第一个参数是要监听的 Dispatch Group。在追加到该 Group 的全部任务处理执行结束时,将第三个参数的 Block 追加到第二个参数的 Dispatch Queue 中。

有时候我们根据具体需求,也可以使用 dispatch_group_enterdispatch_group_leave 两个函数来处理组任务的需求:

-(void)dispatchGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 第 1 个网络请求
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"第 1 个请求结束");
        dispatch_group_leave(group);
    });
    
    // 第 2 个网络请求
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"第 2 个请求结束");
        dispatch_group_leave(group);
    });
    
    // 第 3 个网络请求
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"第 3 个请求结束");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"任务执行完毕了");
    });
}

输出:

2019-12-11 16:16:20.065807+0800 GCD[60651:12053195] 第 1 个请求结束
2019-12-11 16:16:20.065956+0800 GCD[60651:12053192] 第 3 个请求结束
2019-12-11 16:16:20.066017+0800 GCD[60651:12053194] 第 2 个请求结束
2019-12-11 16:16:20.066156+0800 GCD[60651:12053194] 任务执行完毕了

dispatch_group_wait

dispatch_group_wait 配合 dispatch_group 使用,用来判断指定时间范围内,添加的组任务是否全部完成。可根据返回的结果,作区分处理。一共有两种情况:

  • 返回结果为 0 : 组任务完成没有超时。
  • 返回结果不为 0 :组任务执行超时。
任务超时
-(void)dispatchGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0; i < 5; i++) {
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 1000000000; i++) {
                
            }
            NSLog(@"任务ID: %d", i);
        });
    }
    
    long result1 = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
    if (result1 == 0) {
        NSLog(@"result1: group 内部的任务全部结束");
    } else {
        NSLog(@"result1: 在指定时间内,group 内部任务没有全部完成。 result1 = %ld", result1);
    }
}

输出:

2019-12-11 16:35:43.881997+0800 GCD[61895:12119734] result1: 在指定时间内,group 内部任务没有全部完成。 result1 = 49
2019-12-11 16:35:47.280371+0800 GCD[61895:12119867] 任务ID: 2
2019-12-11 16:35:47.338385+0800 GCD[61895:12119863] 任务ID: 0
2019-12-11 16:35:47.346969+0800 GCD[61895:12119865] 任务ID: 1
2019-12-11 16:35:47.356539+0800 GCD[61895:12119864] 任务ID: 3
2019-12-11 16:35:50.670539+0800 GCD[61895:12119867] 任务ID: 4
任务未超时:
-(void)dispatchGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0; i < 5; i++) {
        dispatch_group_async(group, queue, ^{
            NSLog(@"任务ID: %d", i);
        });
    }
    
    long result1 = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
    if (result1 == 0) {
        NSLog(@"result1: group 内部的任务全部结束");
    } else {
        NSLog(@"result1: 在指定时间内,group 内部任务没有全部完成。 result1 = %ld", result1);
    }
}

输出:

2019-12-11 16:37:04.044337+0800 GCD[61979:12123344] 任务ID: 1
2019-12-11 16:37:04.044345+0800 GCD[61979:12123342] 任务ID: 2
2019-12-11 16:37:04.044383+0800 GCD[61979:12123345] 任务ID: 3
2019-12-11 16:37:04.044374+0800 GCD[61979:12123343] 任务ID: 0
2019-12-11 16:37:04.044542+0800 GCD[61979:12123344] 任务ID: 4
2019-12-11 16:37:04.044672+0800 GCD[61979:12123199] result1: group 内部的任务全部结束

如果将时间设置为 DISPATCH_TIME_FOREVER,则进入不设限制时间的等待,直到组任务全部完成,dispatch_group_wait 函数返回结果为 0。如果时间设置为 DISPATCH_TIME_NOW,即为不等待,直接判断返回结果。

虽然利用 RunLoop 循环可以检测任务是否结束,而不消耗多余等待时间,但是书籍作者还是更推荐使用 dispatch_group_notify,可以简化代码。

dispatch_barrier_async

dispatch_barrier_async 又称为栅栏函数,简单来说,并发队列的多个任务,执行顺序是不固定的,但是如果中间插入了 dispatch_barrier_async 函数,就会以此函数为界限,分别执行。举例如下:

-(void)dispatchBarrier {
    dispatch_queue_t queue = dispatch_queue_create("concurrent queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"第 1 次 读取数据");
    });
    dispatch_async(queue, ^{
        NSLog(@"第 2 次 读取数据");
    });
    dispatch_async(queue, ^{
        NSLog(@"第 3 次 读取数据");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"这条数据被修改了,此后需要使用新数据");
    })
    dispatch_async(queue, ^{
        NSLog(@"第 1 次 获取最新数据");
    });
    dispatch_async(queue, ^{
        NSLog(@"第 2 次 获取最新数据");
    });
    dispatch_async(queue, ^{
        NSLog(@"第 3 次 获取最新数据");
    });
}

输出:

2019-12-12 10:21:26.499064+0800 GCD[6242:86373] 第 2 次 读取数据
2019-12-12 10:21:26.499064+0800 GCD[6242:86376] 第 1 次 读取数据
2019-12-12 10:21:26.499099+0800 GCD[6242:86375] 第 3 次 读取数据
2019-12-12 10:21:26.499271+0800 GCD[6242:86376] 这条数据被修改了,此后需要使用新数据
2019-12-12 10:21:26.499412+0800 GCD[6242:86376] 第 1 次 获取最新数据
2019-12-12 10:21:26.499418+0800 GCD[6242:86373] 第 3 次 获取最新数据
2019-12-12 10:21:26.499424+0800 GCD[6242:86375] 第 2 次 获取最新数据

dispatch_sync

目前所有的例子都使用的异步函数,相对的,也有 同步 函数。这里简单区分下:

  • dispatch_async:异步函数,这个函数时立即返回。就是将指定的 Block 非同步 的追加到指定的 Dispatch Queue 中。dispatch_async 不做任何等待。
  • dispatch_sync:同步函数,这个函数不会立即返回。就是将指定的 Block 同步 的追加到指定的 Dispatch Queue 中。再追加的 Block 结束前,dispatch_async 函数会一直等待。

下面举个例子说明:

-(void)dispatchSync {
    // 同步处理
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"开始同步处理");
    __block NSInteger num = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync(queue, ^{
       // 模仿耗时操作
        for (NSInteger i = 0; i< 1000000000; i++) {
            num ++;
        }
        NSLog(@"当前线程:%@", [NSThread currentThread]);
        NSLog(@"同步处理结束");
    });
    NSLog(@"num: %ld", num);
    NSLog(@"当前线程:%@", [NSThread currentThread]);
}

输出:

2019-12-12 10:34:57.951023+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}
2019-12-12 10:34:57.951307+0800 GCD[6908:133503] 开始同步处理
2019-12-12 10:35:00.762171+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}
2019-12-12 10:35:00.762339+0800 GCD[6908:133503] 同步处理结束
2019-12-12 10:35:00.762436+0800 GCD[6908:133503] num: 1000000000
2019-12-12 10:35:00.762595+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}

通过输出的时间可以看到,即使 dispatch_sync 函数内部执行了 3 秒钟,依然要等待执行完,才会继续执行下面的代码。即以阻塞线程的执行。并且始终在主线程。

下面用异步函数执行相同的代码逻辑:

-(void)dispatchAsync {
    // 同步处理
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"开始异步处理");
    __block NSInteger num = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
       // 模仿耗时操作
        for (NSInteger i = 0; i< 1000000000; i++) {
            num ++;
        }
        NSLog(@"当前线程:%@", [NSThread currentThread]);
        NSLog(@"异步处理结束");
    });
    NSLog(@"num: %ld", num);
    NSLog(@"当前线程:%@", [NSThread currentThread]);
}

输出:

2019-12-12 10:40:06.457260+0800 GCD[7190:149132] 当前线程:<NSThread: 0x600003e0dc40>{number = 1, name = main}
2019-12-12 10:40:06.457464+0800 GCD[7190:149132] 开始异步处理
2019-12-12 10:40:06.457614+0800 GCD[7190:149132] num: 0
2019-12-12 10:40:06.457854+0800 GCD[7190:149132] 当前线程:<NSThread: 0x600003e0dc40>{number = 1, name = main}
2019-12-12 10:40:09.495490+0800 GCD[7190:149279] 当前线程:<NSThread: 0x600003e50040>{number = 4, name = (null)}
2019-12-12 10:40:09.497207+0800 GCD[7190:149279] 异步处理结束

可以看到 异步函数 dispatch_async 内部其实已经不在主线程执行了。也未阻塞主线程的执行。

另外,即使将 dispatch_async 后的 Block 追加到串行队列中,依然不会阻塞主线程,因为这是异步函数 dispatch_async 的功能, 和所操作的队列无关。

另外,同步函数非常容易造成死锁,如下:

-(void)deadLock {
    NSLog(@"任务1");
    dispatch_queue_t mainQ = dispatch_get_main_queue();
    dispatch_sync(mainQ, ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
}

上面代码运行时会引发崩溃。该源代码在 Main Dispatch Queue 即主线程中执行追加的 Block,并等待其执行结束。而其实主线程正在执行这些代码,所以无法执行追加到 Main Dispatch Queue 的 Block。

简单来说,因为任务2 被追加到主队列最后,它需要等到任务3 执行结束,但又是同步函数,所以任务3也在等待任务2执行完成。二者相互等待,形成死锁。

对于下面这段代码:

-(void)deadLock {
    NSLog(@"任务1");
    dispatch_queue_t mainQ = dispatch_get_main_queue();
    dispatch_async(mainQ, ^{
        NSLog(@"任务2");
        dispatch_sync(mainQ, ^{
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

输出:

2019-12-12 11:10:09.232553+0800 GCD[8612:258269] 任务1
2019-12-12 11:10:09.233120+0800 GCD[8612:258269] 任务5
2019-12-12 11:10:09.278799+0800 GCD[8612:258269] 任务2

执行后依然会运行时崩溃,这里也形成了死锁。dispatch_async 函数追加的 Block 内部形成死锁。

当我们使用同步函数 dispatch_sync 时,一定要多加注意,否则非常容易引发问题。

dispatch_apply

dispatch_apply 函数是 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按照指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束:

-(void)dispatchApply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"index: %zu", index);
    });
    NSLog(@"apply 执行完毕");
}

输出:

2019-12-12 11:19:16.722810+0800 GCD[9034:290586] index: 1
2019-12-12 11:19:16.722839+0800 GCD[9034:290589] index: 2
2019-12-12 11:19:16.722863+0800 GCD[9034:290584] index: 3
2019-12-12 11:19:16.723002+0800 GCD[9034:290589] index: 5
2019-12-12 11:19:16.723002+0800 GCD[9034:290586] index: 4
2019-12-12 11:19:16.723009+0800 GCD[9034:290584] index: 6
2019-12-12 11:19:16.723104+0800 GCD[9034:290589] index: 7
2019-12-12 11:19:16.723151+0800 GCD[9034:290586] index: 8
2019-12-12 11:19:16.723204+0800 GCD[9034:290584] index: 9
2019-12-12 11:19:16.722801+0800 GCD[9034:290372] index: 0
2019-12-12 11:19:16.726093+0800 GCD[9034:290372] apply 执行完毕

我们也可以用这个函数来遍历数组:

-(void)dispatchApply {
    NSArray *array = @[@1,@10,@43,@13,@33];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index) {
        NSLog(@"%@",array[index]);
    });
    NSLog(@"数组遍历完毕");
}

输出:

2019-12-12 11:23:15.304275+0800 GCD[9223:304025] 1
2019-12-12 11:23:15.304278+0800 GCD[9223:304185] 10
2019-12-12 11:23:15.304346+0800 GCD[9223:304184] 43
2019-12-12 11:23:15.304367+0800 GCD[9223:304183] 13
2019-12-12 11:23:15.304471+0800 GCD[9223:304185] 33
2019-12-12 11:23:15.304623+0800 GCD[9223:304025] 数组遍历完毕

可以看到 dispatch_apply 函数同 dispatch_sync 函数一样,都会阻塞线程,所以推荐在 dispatch_async 函数中非同步的执行 dispatch_apply 函数:

- (void)dispatch_apply_3
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        
        NSArray *array = @[@1,@10,@43,@13,@33];
        __block  NSInteger sum = 0;
    
        dispatch_apply([array count], queue, ^(size_t index) {
            NSNumber *number = array[index];
            NSInteger num = [number integerValue];
            sum += num;
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主线程,拿到总和
            NSLog(@"完毕");
            NSLog(@"%ld",sum);
        });
    });
}

dispatch_suspend/dispatch_resume

dispatch_suspend 函数挂起指定的 Dispatch Queue:

dispatch_suspend(queue);

dispatch_resume 函数恢复指定的 Dispatch Queue:

dispatch_resume(queue);

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

Dispatch Semaphore

GCD 的信号量函数,相比 Serial Dispatch Queue 和 dispatch_barrier_async,信号量函数有着更细粒度的排他控制。

dispatch_semaphore_create

dispatch_semaphore_create 函数用来创建信号量:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

这是一个可失败的构造器,如果参数 value 的值小于等于0,则创建失败,对于参数 value 注释如下:

The starting value for the semaphore. Passing a value less than zero will cause NULL to be returned.

dispatch_semaphore_wait

dispatch_semaphore_wait 返回一个 long 值,Returns zero on success, or non-zero if the timeout occurred. ,返回 0 时,成功,非 0 则失败:

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

其中第二个参数与 dispatch_group_wait 函数的参数相同。

Dispatch Semaphore 示例

下面使用一个 for 循环中的异步函数来看下 Dispatch Semaphore 的特点:

-(void)dispatchSemaphore {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 信号量的初始值设置为 4
    // 保证可访问 NSMUtableArray 类对象的线程同时只能有 1 个!
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            // 等待 Dispatch Semaphore
            // 这里设置一直等待,直到 Dispatch Semaphore 的计数值达到大于等于 1
            NSLog(@"信号量等待前 index = %d", i);
            long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"信号量等待结束  result: %ld     index = %d", result, i);
            
            /*
             * 由于 Dispatch Semaphore 的计数值达到大于等于 1
             * 所以将 Dispatch Semaphore 的计数值减去 1
             * dispatch_semaphore_wait 函数执行返回
             *
             * 执行到此时,Dispatch Semaphore 的计数值恒为 0。
             *
             * 由于可访问 NSMUtableArray 类对象的线程只有一个,因此可以安全的更新
             */
            [array addObject:[NSNumber numberWithInt:i]];
            
            /*
             * 排他控制结束
             * 所以通过函数 dispatch_semaphore_signal 函数将 Dispatch Semaphore 的计数值增加 1。
             *
             * 如果有通过 dispatch_semaphore_wait 函数等待 Dispatch Semaphore 的计数值 增加的线程,
             * 就由最先等待的线程结束
             */
            dispatch_semaphore_signal(semaphore);
        });
    }
    
    NSLog(@"数组操作结束  array count = %ld", [array count]);
}

输出:

2019-12-12 14:24:27.972120+0800 GCD[17626:950806] 数组操作结束  array count = 0
2019-12-12 14:24:27.972120+0800 GCD[17626:950949] 信号量等待前 index = 0
2019-12-12 14:24:27.972120+0800 GCD[17626:950950] 信号量等待前 index = 1
2019-12-12 14:24:27.972157+0800 GCD[17626:950951] 信号量等待前 index = 2
2019-12-12 14:24:27.972181+0800 GCD[17626:950948] 信号量等待前 index = 3
2019-12-12 14:24:27.972349+0800 GCD[17626:950984] 信号量等待前 index = 5
2019-12-12 14:24:27.972266+0800 GCD[17626:950983] 信号量等待前 index = 4
2019-12-12 14:24:27.972364+0800 GCD[17626:950949] 信号量等待结束  result: 0     index = 0
2019-12-12 14:24:27.972443+0800 GCD[17626:950951] 信号量等待结束  result: 0     index = 2
2019-12-12 14:24:27.972449+0800 GCD[17626:950950] 信号量等待结束  result: 0     index = 1
2019-12-12 14:24:27.972464+0800 GCD[17626:950948] 信号量等待结束  result: 0     index = 3
2019-12-12 14:24:27.973015+0800 GCD[17626:950949] 信号量等待前 index = 6
2019-12-12 14:24:27.973104+0800 GCD[17626:950984] 信号量等待结束  result: 0     index = 5
2019-12-12 14:24:27.973202+0800 GCD[17626:950985] 信号量等待前 index = 7
2019-12-12 14:24:27.973626+0800 GCD[17626:950951] 信号量等待前 index = 8
2019-12-12 14:24:27.973750+0800 GCD[17626:950983] 信号量等待结束  result: 0     index = 4
2019-12-12 14:24:27.974067+0800 GCD[17626:950948] 信号量等待前 index = 9
2019-12-12 14:24:27.974094+0800 GCD[17626:950949] 信号量等待结束  result: 0     index = 6
2019-12-12 14:24:27.975372+0800 GCD[17626:950951] 信号量等待结束  result: 0     index = 8
2019-12-12 14:24:27.975698+0800 GCD[17626:950948] 信号量等待结束  result: 0     index = 9
2019-12-12 14:24:27.976428+0800 GCD[17626:950985] 信号量等待结束  result: 0     index = 7

这里针对 dispatch_semaphore_create 函数的参数简单说明下,它像是 信号量执行前的预等待次数,如果写1, 数组操作结束 输出打印较晚,如果数值较大,譬如 4, 数组操作结束 输出就会优先打印出来。

dispatch_once

dispatch_once 函数是保证应用程序执行中只执行一次指定处理的 API,而且在多线程环境下执行,也是线程安全的。主要用于单例模式。

- (void)onceCode
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"只执行一次的代码");
    });
}

Dispatch I/O

Dispatch I/O 主要用于提高文件的读取速度。

总结

《Objective-C高级编程》这本书一共三章,书虽然买了几年了,但确实是很正经的完整读了第一遍,这里做个笔记,方便以后来看看吧。

《Objective-C高级编程》三篇总结之一:引用计数篇

《Objective-C高级编程》三篇总结之二:Block篇

《Objective-C高级编程》三篇总结之三:GCD篇

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