美文网首页
《Objective-C高级编程》三篇总结之三:GCD篇

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

作者: 四月_Hsu | 来源:发表于2019-12-13 16:36 被阅读0次

    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篇

    相关文章

      网友评论

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

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