Objective-C的GCD部分API学习笔记

GCD

GCD为Grand Central Dispatch的缩写。Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。

GCD是一个替代诸如NSThread等技术的很高效和强大的技术。GCD完全可以处理诸如数据锁定和资源泄漏等复杂的异步编程问题。GCD的工作原理是让一个程序,根据可用的处理资源,安排他们在任何可用的处理器核心上平行排队执行特定的任务。这个任务可以是一个功能或者一个程序段

GCD仍然在一个很低的水平使用线程,但是它不需要程序员关注太多的细节。GCD创建的队列是轻量级的,苹果声明一个GCD的工作单元需要由15个指令组成。也就是说创造一个传统的线程很容易的就会需要几百条指令。

GCD中的一个任务可被用于创造一个被放置于队列的工作项目或者事件源。如果一个任务被分配到一个事件源,那么一个由功能或者程序块组成的工作单元会被放置于一个适当的队列中。苹果公司认为GCD相比于普通的一个接一个的执行任务的方式更为有效率。

GCD - 定时器

IOS开发中常用的定时器之一,以纳秒作为时间间隔单位,且运行不受RunLoop的运行模式的影响,相比NStimer定时器运行更为精准,Xcode自带的代码块,直接dispatch就可联想出来,传入参数即可,需要注意其本质为结构体,需要定义一个强指针(strong)修饰的对象引用它,否则它就会被销毁。

强引用声明dispatch_source_t对象,要这样声明@property (nonatomic, strong) dispatch_source_t timer;

\color{red}{开启定时器代码块:}

__weak typeof(self) weakSelf = self;
dispatch_queue_t queue = dispatch_get_main_queue();
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
        //执行的函数或代码块
        [weakSelf countDownNotification];
    });
dispatch_resume(_timer);

\color{red}{关闭定时器代码块:}

-(void)dealloc{
    if (self.timer) {
        dispatch_cancel(self.timer);
        self.timer = nil;
        LCCKLog(@"timer has been destoryed!");
    }
}

定时器代码块中的代码作用:

    1. 创建队列函数
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

函数描述:dispatch_get_main_queue()函数,用于调度获取主队列,返回绑定到主线程的默认队列,当用于非UI应用程序的进程时,它可能会产生不必要的副作用,对于这样的进程,应该避免使用主队列。

返回值:返回主队列。

例如:

dispatch_queue_t queue = dispatch_get_main_queue();
    1. 创建GCD中的定时器
dispatch_source_create(dispatch_source_type_t type,
    uintptr_t handle,
    unsigned long mask,
    dispatch_queue_t _Nullable queue);

函数描述:dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t _Nullable queue)函数,用于创建一个新的调度源来监视底层系统对象,并自动地向调度队列提交一个处理程序块以响应事件

调度源不可重入,在调度源挂起或事件处理程序块当前正在执行时接收到的任何事件都会在调度源恢复或事件处理程序块返回后合并和传递

调度源在挂起状态下创建,创建源并设置任何所需属性(即处理程序、上下文等)之后,必须调用dispatch_activate() ,以开始事件传递

参数 :

dispatch_source_type_t type :调度源的类型。例如要创建计时器源,需要指定 DISPATCH_SOURCE_TYPE_TIMER。有关常量的完整列表,可以参阅 dispatch_source_type_t。

uintptr_t handle :要监视的底层系统句柄,这个参数的解释由类型参数中提供的常量决定。

unsigned long mask :指定需要哪些事件的标志掩码,这个参数的解释由类型参数中提供的常量决定。

dispatch_queue_t _Nullable queue :事件处理程序块将提交到的调度队列,如果队列是DISPATCH_TARGET_QUEUE_DEFAULT,那么源将把事件处理程序块提交给默认的优先级全局队列。

返回值 : 新的调度源对象,如果无法创建调度源,则为null。

例如:

 /*
     第一个参数:创建source的类型 DISPATCH_SOURCE_TYPE_TIMER:定时器
     第二个参数:0
     第三个参数:0
     第四个参数:队列
*/
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    1. 设置定时器
dispatch_source_set_timer(dispatch_source_t source,
    dispatch_time_t start,
    uint64_t interval,
    uint64_t leeway);

函数描述:dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway) 函数,用于设置计时器源的开始时间、间隔和回旋余地值(允许的误差值,0表示尽量精准)

为了提高系统的性能,定时器的任何触发都可能被系统延迟。允许延迟的上限可以配置为允许的误差值(leeway参数的值),下限由系统控制。如果指定的计时器源是使用DISPATCH_TIMER_STRICT掩码创建的,那么系统将尽最大努力严格遵守所提供的leeway值,即使它小于当前的下限。需要注意的是即使指定了 leeway 值为0,所有计时器也会出现一些延迟

start参数还决定计时器将使用哪个时钟,如果start参数是DISPATCH_TIME_NOW或使用dispatch_time创建的,则计时器基于运行时间(在苹果平台上从mach_absolute_time()获得)。如果start是使用dispatch_walltime()创建的,那么计时器将基于gettimeofday()。

如果计时器源已被取消,则调用此函数无效

参数 :

dispatch_source_t source:定时器源对象。

dispatch_time_t start : 计时器的开始时间。例如计时器在创建后第一次执行任务是在创建5秒后,参数则传入dispatch_walltime(NULL, 5.0 * NSEC_PER_SEC)。

uint64_t interval : 计时器的纳秒间隔。对于一次性计时器,使用DISPATCH_TIME_FOREVER。

uint64_t leeway : 计时器允许的误差时间,纳秒。

例如:

 /*
     第一个参数:定时器对象
     第二个参数:DISPATCH_TIME_NOW 表示从现在开始计时
     第三个参数:间隔时间 GCD里面的时间 纳秒
     第四个参数:误差值(表示允许的误差,0表示尽量精准)
*/
// * NSEC_PER_SEC 转换为秒
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    1. 设置事件
dispatch_source_set_event_handler(dispatch_source_t source,
    dispatch_block_t _Nullable handler);

函数描述:dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler)函数,用于为给定的调度源设置事件处理程序块

参数 :

dispatch_source_t source : 要修改的调度源,此参数不能为空。

dispatch_block_t _Nullable handler : 要提交到源的目标队列的事件处理程序块。

例如:

dispatch_source_set_event_handler(_timer, ^{
        [weakSelf countDownNotification];
    });
    1. 开始执行
dispatch_resume(dispatch_object_t object);

函数描述:dispatch_resume(dispatch_object_t object)函数,用于恢复对调度对象上的块的调用。可以使用dispatch_suspend()可以挂起分派对象,这会增加内部挂起计数。dispatch_resume()是相反的操作,使用它减少内部挂起计数。当挂起计数返回零时,将再次调用与对象关联的块

由dispatch_source_create返回的新调度事件源对象的挂起计数为1,并且必须在传递任何事件之前恢复,这种为新调度事件源对象的挂起计数设置为1的方法允许应用程序在交付第一个事件之前完全配置调度事件源对象。除了上述这种例外,对dispatch_resume()的每次调用都必须平衡对dispatch_suspend()的调用,如果调用dispatch_resume()的次数比调用dispatch_suspend()的次数多,这将导致挂起计数为负,这种行为是未定义的

出于向后兼容性的原因,dispatch_resume() 在非活动且未挂起的调度源对象上与调用dispatch_activate()具有相同的效果。对于新代码,最好使用dispatch_activate()

参数:

dispatch_object_t object : 要恢复的对象。在此参数中传递null的结果是未定义的。

例如:

//开始执行
dispatch_resume(_timer);
    1. 关闭定时器
dispatch_cancel(object)

函数描述dispatch_cancel()是通过第一个参数的类型去映射到dispatch_block_cancel或 dispatch_source_cancel的类型通用宏,此函数不适用于任何其他对象类型。

参数:

object :要取消的对象。在此参数中传递null的结果是未定义的。

例如:

dispatch_cancel(self.timer);

\color{red}{GCD计时器实现的简单倒计时按钮:}

#import <UIKit/UIKit.h>

@interface CountdownButton : UIButton

- (void)startCountdownCompletionhander:(void (^)(void))completionHanlder;

@end
#import "CountdownButton.h"

@interface CountdownButton()

@property (strong, nonatomic) dispatch_source_t timer;//强引用声明dispatch_source_t对象,否则它就会被销毁。

@end


@implementation CountdownButton

- (void)dealloc {
    //定时器失效
    [self timerInvalidate];
}

///类方法初始化按钮
+ (instancetype)buttonWithType:(UIButtonType)buttonType {
    
    CountdownButton *button = [super buttonWithType:buttonType];
    //按钮背景色
    button.backgroundColor = HEXCOLOR(0xF56456);
    //按钮标题字体大小
    button.titleLabel.font = [UIFont systemFontOfSize:13];
    //按钮圆角
    button.layer.cornerRadius = 3.0;
    //按钮超出层边界裁剪
    button.layer.masksToBounds = YES;
    //可以减小按钮标题字体大小以适应
    button.titleLabel.adjustsFontSizeToFitWidth = YES;
    //可以减小的最小比例
    button.titleLabel.minimumScaleFactor = 0.8;
    //设置按钮标题文本
    [button setTitle:@"获取验证码" forState:UIControlStateNormal];
    //设置按钮标题文本颜色
    [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    
    return button;
}

///初始化按钮
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
        //按钮背景色
        self.backgroundColor = HEXCOLOR(0xF56456);
        //按钮标题字体大小
        self.titleLabel.font = [UIFont systemFontOfSize:13];
        //按钮圆角
        self.layer.cornerRadius = 3.0;
        //按钮超出层边界裁剪
        self.layer.masksToBounds = YES;
        //可以减小按钮标题字体大小以适应
        self.titleLabel.adjustsFontSizeToFitWidth = YES;
         //可以减小的最小比例
        self.titleLabel.minimumScaleFactor = 0.8;
        //设置按钮标题文本
        [self setTitle:@"获取验证码" forState:UIControlStateNormal];
        //设置按钮标题文本颜色
        [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    }
    return self;
}

///开始倒计时并在完成时处理程序
- (void)startCountdownCompletionhander:(void (^)(void))completionHanlder {
    //倒计时时间(秒)
    __block NSTimeInterval time = 5.0;
    //如果self.timer为null,则进行创建
    if (self.timer == NULL) {
        //创建GCD中的定时器
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
        //设置定时器
        dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        //设置事件
        dispatch_source_set_event_handler(self.timer, ^{
            //按钮是否启用
            BOOL enabled;
            //背景色
            UIColor *backgroundColor;
            //标题
            NSString *title;
            //标题颜色
            UIColor *titleColor;
            
            if (time <= 0) {
                //倒计时结束
                //定时器失效
                [self timerInvalidate];
                //设置按钮响应属性
                enabled = YES;
                backgroundColor = HEXCOLOR(0xF56456);
                title = @"获取验证码";
                titleColor = [UIColor whiteColor];
                
            } else {
                
                enabled = NO;
                backgroundColor = HEXCOLOR(0xEEEEEE);
                title = [NSString stringWithFormat:@"%2.0f秒后再次发送", time];
                titleColor = HEXCOLOR(0xBDBDBD);
            }
            
            time -= 1.0;
            
            //获取主队列,修改按钮样式
            dispatch_async(dispatch_get_main_queue(), ^{
                
                //将响应属性设置到按钮
                self.enabled = enabled;
                self.backgroundColor = backgroundColor;
                [self setTitle:title forState:UIControlStateNormal];
                [self setTitleColor:titleColor forState:UIControlStateNormal];
                
                //判断是否执行倒计时完成块
                if (self.isEnabled &&
                    completionHanlder) {
                    completionHanlder();
                }
            });
        });
        //开始执行计时器
        dispatch_resume(self.timer);
    }
}

///定时器失效
- (void)timerInvalidate {
    
    if (self.timer) {
        dispatch_cancel(self.timer);
    }
    self.timer = NULL;
}


@end

调用代码:

#import "CountdownButton.h"

@interface TestCodeController ()

@property (nonatomic, strong) CountdownButton *countdownButton;

@end

@implementation TestCodeController

- (void)viewDidLoad {

    [super viewDidLoad];
    
    ///倒计时按钮
    self.countdownButton = [[CountdownButton alloc]initWithFrame:CGRectZero];
    [self.countdownButton addTarget:self action:@selector(againCountdown) forControlEvents:UIControlEventTouchUpInside];
    [self.countdownButton startCountdownCompletionhander:^{
        [self showAlert];
    }];
    
    [self.view addSubview:self.countdownButton];
    [self.countdownButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(90, 35));
    }];
    
}

///再次倒计时
- (void)againCountdown{
    [self.countdownButton startCountdownCompletionhander:^{
        [self showAlert];
    }];
}

///显示提示
- (void)showAlert{
    UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"倒计时结束" message:nil preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *findAction = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
    }];
    [alertView addAction:findAction];
    [self presentViewController:alertView animated:YES completion:nil];
}

@end

样式如下:

Jietu20201117-224536.gif

GCD - 延迟执行

dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
        dispatch_block_t block);

函数描述:在指定的时间排队执行一个块。此函数等待到指定的时间,然后异步地将块添加到指定的队列中。支持将DISPATCH_TIME_NOW作为when参数传递,但不如调用dispatch_async来代替。传递DISPATCH_TIME_FOREVER的戏行为未定义。

参数 :

when :触发事件的时间,通常由dispatch_time或dispatch_walltime函数返回。

queue :提交块的队列。系统将保留队列,直到块运行完成。该参数不能为NULL。

block :要提交的块。这个函数代表调用方执行Block_copy和Block_release。该参数不能为NULL。

\color{red}{GCD延迟执行block(块)中代码:}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"要执行的代码");
});

GCD - 调度队列

GCD调度队列是执行任务的强大工具。调度队列允许相对于调度者异步或者同步的执行任意代码块。可以使用调度队列来执行几乎所有在单独线程上执行的任务。调度队列的优点是它们比线程代码更简单且更高效。

调度队列是在应用程序中异步并发执行任务的一种简单方法。任务通常是应用程序需要执行的一些工作。例如,可能是定义一个任务来执行一些计算,创建和修改数据结构,处理从文件中读取的数据,或者任意数量的事情。通过放置相应的代码到函数或者块对象中来定义任务,并把他们放到调度队列中。

调度队列是一个类似对象的结构体,它管理提交给它的任务。所有的调度队列都是FIFO(先进先出)的数据结构。因此,添加到队列的任务始终以添加他们的相同顺序开始执行。GCD自动提供一些调度队列,但也可以为特定目的创建其他队列。

调度队列按FIFO(先进先出)顺序连续调用提交给它的块。一个串行队列一次只调用一个块,但是每个独立的队列可以相对于其他队列并发地调用它们的块。

全局并发队列以FIFO(先进先出)顺序调用块,但不等待它们的完成,允许并发调用多个块。

系统管理一个线程池,用于处理调度队列和调用提交给它们的块。从概念上讲,调度队列可以有自己的执行线程,并且队列之间的交互是高度异步的。
调度队列通过调用dispatch_retain和dispatch_release进行引用计数。提交给队列的挂起块还保留对队列的引用,直到它们完成为止。一旦对队列的所有引用都被释放,该队列将被系统释放。

typedef NSObject<OS_dispatch_queue>*dispatch_queue_t;

类型描述:应用程序向其提交块以供后续执行的轻量级对象。

typedef NSObject<OS_dispatch_queue_main> *dispatch_queue_main_t;

类型描述:一个绑定到应用程序主线程并在该主线程上串行执行任务的调度队列。

typedef NSObject<OS_dispatch_queue_global> *dispatch_queue_global_t;

类型描述:使用全局线程池中的线程并发执行任务的调度队列。

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

函数描述在调度队列上提交一个用于异步执行的块并立即返回。这个函数是向调度队列提交块的基本机制。对该函数的调用总是在块提交后立即返回,从不等待块被调用,而目标队列决定该块与被提交到该同一队列的其他块串行调用还是并发调用。而独立的串行队列彼此之间被并发地处理。

参数 :

queue :要提交块的队列。系统将保留队列,直到块运行完成。此参数不能为NULL。

block : 要提交给目标调度队列的块。这个函数代表调用方执行Block_copy和Block_release。该参数不能为NULL。

dispatch_queue_main_t
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

函数描述返回与应用程序的主线程关联的串行调度队列。系统会自动创建主队列,并将其与应用程序的主线程相关联。应用程序使用以下三种方法中的一种(仅一种)来调用提交给主队列的块:

  • 1.调用dispatch_main
  • 2.调用UIApplicationMain (iOS)或NSApplicationMain (macOS)
  • 3.在主线程上使用CFRunLoopRef

与全局并发队列一样,对dispatch_suspend、dispatch_resume、dispatch_set_context等函数的调用在此函数返回的队列中使用时没有效果。

返回值 : 返回主队列。这个队列是在调用main之前自动代表主线程创建的。

dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);

函数描述返回系统定义的具有指定的服务质量的全局并发队列。该函数返回适合执行具有指定服务质量级别的任务的队列。调用dispatch_suspend、dispatch_resume和dispatch_set_context函数对返回的队列没有影响。提交到返回队列的任务是相对于其他任务并行调度的。

参数 :

identifier :希望为使用此队列执行的任务提供的服务质量。服务质量有助于确定队列执行的任务的优先级。可以指定QOS_CLASS_USER_INTERACTIVE、QOS_CLASS_USER_INITIATED、QOS_CLASS_UTILITY或QOS_CLASS_BACKGROUND。处理用户交互或用户发起任务的队列(指定为QOS_CLASS_USER_INTERACTIVE)比在后台运行的任务(指定为QOS_CLASS_BACKGROUND)具有更高的优先级。

注 :在OS X 10.9或更早版本中,可以指定一个调度队列优先级值,可以在dispatch_queue_priority_t中找到。这些值映射到适当的服务质量类。可以指定DISPATCH_QUEUE_PRIORITY_HIGH(任务以最高优先级运行,映射到QOS_CLASS_USER_INTERACTIVE)、DISPATCH_QUEUE_PRIORITY_DEFAULT(任务以默认优先级运行,映射到QOS_CLASS_DEFAULT)、DISPATCH_QUEUE_PRIORITY_LOW(任务以低优先级运行,映射到QOS_CLASS_UTILITY)、DISPATCH_QUEUE_PRIORITY_BACKGROUND(任务以后台优先级运行,映射到QOS_CLASS_BACKGROUND)。

flags : 为将来使用保留的标志。这个参数总是指定0。

返回值 :请求的全局并发队列。

dispatch_semaphore_t - 调度信号量对象

dispatch_semaphore_create(long value);

函数描述使用初始值创建新的dispatch_semaphore_t(调度信号量对象),其dispatch_semaphore_t(调度信号量对象)上初始计数为给定的值。当两个线程需要协调特定事件的完成时,为该value传递0是很有用的。传递大于零的值对于管理有限的资源池非常有用,其中池的大小等于该值。调用dispatch_semaphore_signal()必须与调用dispatch_semaphore_wait()保持平衡

参数 :

value : dispatch_semaphore_t(调度信号量对象)上计数的初始值,不要传递小于0的值。

返回值 : 新创建的dispatch_semaphore_t(调度信号量对象)。

例如:使用初始值创建新的dispatch_semaphore_t(调度信号量对象):

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);

函数描述dispatch_semaphore_t(调度信号量对象)上增加一个计数,调用此函数后如果计数大于等于0,则此函数将唤醒当前在dispatch_semaphore_wait()中等待的线程。

参数 :

dsema : dispatch_semaphore_t对象,此参数不能为空。

返回值 : 如果dispatch_semaphore_t对象上计数大于等于0,则此函数将唤醒当前正在等待的线程。

long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

函数描述等待(衰减)信号量,dispatch_semaphore_t对象上减少一个计数,如果结果值小于0,则此函数将阻塞当前线程并等待信号。

参数 :

dsema : dispatch_semaphore_t对象,此参数不能为空。

timeout : 何时超时,常量DISPATCH_TIME_NOW和DISPATCH_TIME_FOREVER可以方便地使用。

返回值 : 如果成功,则返回0;如果超时,则返回非0。

\color{red}{一个信号量操作及其简单的小例子:}

注:

#define DISPATCH_TIME_NOW (0ull)  //分派时间表示“现在”。
#define DISPATCH_TIME_FOREVER (~0ull)  //分派时间表示“无限”。
- (void)viewDidLoad {

    [super viewDidLoad];
    //使用初始值创建新的计数信号量。
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    //开辟子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"我输出后线程就被阻塞了");
        //等待(减少)信号量。阻塞线程
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"我需等待3秒才能输出");
    });
    //延迟3秒执行函数
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //信号量(递增),唤醒线程
        dispatch_semaphore_signal(semaphore);
    });
}

输出如下 :

截屏2021-01-11下午11.52.23.png

dispatch_group_t - 调度组

dispatch_group_t(调度组)是监视一组块的机制。应用程序可以根据需要同步或异步监视组中的块。通过扩展,组可以用于同步依赖于其他任务完成情况的代码。组中的块可以在不同的队列上运行,每个块可以向组中添加更多块。调度组会跟踪有多少块未完成,GCD保留该组,直到其所有相关块完成执行。

\color{red}{先看一个简单的例子,模拟网络请求,在所有任务完成之后计算number结果:}

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger number = 0;
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(3);
        number += 1;
        NSLog(@"我耗时操作执行了");
    });
    
    //任务B
    dispatch_group_enter(group);
    [self sendTaskTwoRequestWithCompletion:^(id response) {
        number += [response integerValue];
        NSLog(@"我任务B执行了");
        dispatch_group_leave(group);
    }];
    
    //任务C
    dispatch_group_enter(group);
    [self sendTaskThreeRequestWithCompletion:^(id response) {
        number += [response integerValue];
        NSLog(@"我任务C执行了");
        dispatch_group_leave(group);
    }];
    
    //所有任务都执行了
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任务执行完毕,任务结果 ----- %zd", number);
    });
}

- (void)sendTaskTwoRequestWithCompletion:(void (^)(id response))completion {
    //模拟一个网络请求
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        sleep(2);
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) completion(@2);
        });
    });
}

- (void)sendTaskThreeRequestWithCompletion:(void (^)(id response))completion {
    //模拟一个网络请求
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        sleep(1);
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) completion(@3);
        });
    });
}

@end

打印结果如下:

2021-11-09 08:39:13.714426+0800 TestProduct[23932:898935] 我任务C执行了
2021-11-09 08:39:14.714474+0800 TestProduct[23932:898935] 我任务B执行了
2021-11-09 08:39:15.714368+0800 TestProduct[23932:899324] 我耗时操作执行了
2021-11-09 08:39:15.714741+0800 TestProduct[23932:898935] 任务执行完毕,任务结果 ----- 6
dispatch_group_t dispatch_group_create(void);

函数描述创建一个可以为其分配块对象的新调度组,块对象可以与之关联(通过使用dispatch_group_asyn()函数),调度组维护其未完成的关联任务的计数,在关联新任务时增加计数,并在任务完成时减少计数。dispatch_group_notify()和dispatch_group_wait()等函数使用该计数来允许应用程序确定与组关联的所有任务何时完成, 那时应用程序可以采取任何适当的操作。

返回值 : 新创建的dispatch_group_t调度组,失败时为null。

void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

函数描述异步调度一个块以执行并同时将其与指定的调度组关联,调度组会跟踪它引用的块对象的完成。

参数 :

group :与提交的块对象相关联的调度组。 该组由系统保留,直到块运行完成, 此参数不能为null。

queue :块对象被提交到异步调用的调度队列。 队列由系统保留,直到块运行完成,此参数不能为null。

block:要异步执行的块对象。此函数代表调用者执行Block_copy和Block_release。

void dispatch_group_enter(dispatch_group_t group);

函数描述明确表示一个块已进入组,调用此函数会增加组中未完成任务的当前计数。 如果应用程序通过使用dispatch_group_async()函数以外的方式显式添加和删除组中的任务,则使用此函数(与 dispatch_group_leave()函数一起)允许应用程序正确管理任务引用计数。对该函数的调用必须与对dispatch_group_leave 的调用相平衡。可以使用此功能将一个块同时与多个组关联。

参数 :

group :要更新的调度组,此参数不能为null。

void dispatch_group_leave(dispatch_group_t group);

函数描述明确表示组中的一个块已完成执行,调用此函数会减少组中未完成任务的当前计数。 如果应用程序通过使用dispatch_group_async()函数以外的方式显式添加和删除组中的任务,则使用此函数(与 dispatch_group_enter 一起)允许应用程序正确管理任务引用计数。对该函数的调用必须平衡对 dispatch_group_enter 的调用。 调用次数多于dispatch_group_enter 是无效的,这会导致计数为负数

参数 :

group :要更新的调度组,此参数不能为null。

void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

函数描述当与调度组关联的所有块都完成时,此函数安排一个通知块提交到指定的队列。如果调度组为空(没有块对象与调度组关联),则立即提交通知块对象。提交通知块时,该组为空,该组可以使用dispatch_release()函数释放或重新用于其他块对象。

参数 :

group :进行观察的调度组。该组由系统保留,直到块运行完成,此参数不能为null。

queue :调度组完成时将提供的块提交到的队列。队列由系统保留,直到块运行完成,此参数不能为null。

block :组完成时要提交的块。此函数代表调用者执行 Block_copy和Block_release,此参数不能为null。

调度组相关函数源码解析 - (摘抄自NeroXie的深入理解GCD之dispatch_group链接:https://www.jianshu.com/p/e93fd15d93d3)

dispatch_group_t的实现是基于dispatch_semaphore_t的。下面通过源码查看一下dispatch_group_t的相关API。

dispatch_group_create源码

dispatch_group_t
dispatch_group_create(void)
{
    return (dispatch_group_t)dispatch_semaphore_create(LONG_MAX);
}

dispatch_group_async是对dispatch_group_async_f的封装,dispatch_group_async_f源码

void
dispatch_group_async_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt,
        dispatch_function_t func)
{
    dispatch_continuation_t dc;

    _dispatch_retain(dg);
    dispatch_group_enter(dg);

    dc = fastpath(_dispatch_continuation_alloc_cacheonly());
    if (!dc) {
        dc = _dispatch_continuation_alloc_from_heap();
    }

    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_GROUP_BIT);
    dc->dc_func = func;
    dc->dc_ctxt = ctxt;
    dc->dc_group = dg;

    // No fastpath/slowpath hint because we simply don't know
    if (dq->dq_width != 1 && dq->do_targetq) {
        return _dispatch_async_f2(dq, dc);
    }

    _dispatch_queue_push(dq, dc);
}

从上面的代码我们可以看出dispatch_group_async_fdispatch_async_f相似。dispatch_group_async_f多了dispatch_group_enter(dg);,另外在do_vtable的赋值中dispatch_group_async_f多了一个DISPATCH_OBJ_GROUP_BIT的标记符。既然添加了dispatch_group_enter必定会存在dispatch_group_leave。在之前《深入理解GCD之dispatch_queue》介绍_dispatch_continuation_pop函数的源码中有一段代码如下:

    _dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
    if (dg) {
        //group需要进行调用dispatch_group_leave并释放信号
        dispatch_group_leave(dg);
        _dispatch_release(dg);
    }

所以dispatch_group_async_f函数中的dispatch_group_leave是在_dispatch_continuation_pop函数中调用的。

这里概括一下dispatch_group_async_f的工作流程:

  1. 调用dispatch_group_enter
  2. 将block和queue等信息记录到dispatch_continuation_t结构体中,并将它加入到group的链表中;
  3. _dispatch_continuation_pop执行时会判断任务是否为group,是的话执行完任务再调用dispatch_group_leave以达到信号量的平衡。

dispatch_group_enter源码

void
dispatch_group_enter(dispatch_group_t dg)
{
    dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;

    (void)dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
}

dispatch_group_enterdispatch_group_t转换成dispatch_semaphore_t,并调用dispatch_semaphore_wait,原子性减1后,进入等待状态直到有信号唤醒。所以说dispatch_group_enter就是对dispatch_semaphore_wait的封装。

dispatch_group_leave源码

void
dispatch_group_leave(dispatch_group_t dg)
{
    dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;
    dispatch_atomic_release_barrier();
    long value = dispatch_atomic_inc2o(dsema, dsema_value);//dsema_value原子性加1
    if (slowpath(value == LONG_MIN)) {//内存溢出,由于dispatch_group_leave在dispatch_group_enter之前调用
        DISPATCH_CLIENT_CRASH("Unbalanced call to dispatch_group_leave()");
    }
    if (slowpath(value == dsema->dsema_orig)) {//表示所有任务已经完成,唤醒group
        (void)_dispatch_group_wake(dsema);
    }
}

从上面的源代码中我们看到dispatch_group_leavedispatch_group_t转换成dispatch_semaphore_t后将dsema_value的值原子性加1。如果valueLONG_MIN 程序crash;如果value等于dsema_orig表示所有任务已完成,调用_dispatch_group_wake唤醒group(_dispatch_group_wake的作用和notify有关,我们会在后面介绍)。因为在enter的时候进行了原子性减1操作。所以在leave的时候需要原子性加1。

这里先说明一下 enterleave 之间的关系:

dispatch_group_leavedispatch_group_enter 配对使用。当调用了 dispatch_group_enter 而没有调用 dispatch_group_leave 时,由于 value 不等于 dsema_orig 不会走到唤醒逻辑,dispatch_group_notify 中的任务无法执行或者dispatch_group_wait收不到信号而卡住线程。

dispatch_group_enter 必须在 dispatch_group_leave 之前出现。当 dispatch_group_leavedispatch_group_enter 多调用了一次或者说在 dispatch_group_enter 之前被调用的时候,dispatch_group_leave 进行原子性加1操作,相当于 valueLONGMAX+1,发生数据长度溢出,变成 LONG_MIN,由于 value == LONG_MIN 成立,程序发生crash。

dispatch_group_notify是dispatch_group_notify_f的封装,具体实现在后者

void
dispatch_group_notify_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt,
        void (*func)(void *))
{
    dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;
    struct dispatch_sema_notify_s *dsn, *prev;

    //封装dispatch_continuation_t结构体
    // FIXME -- this should be updated to use the continuation cache
    while (!(dsn = calloc(1, sizeof(*dsn)))) {
        sleep(1);
    }

    dsn->dsn_queue = dq;
    dsn->dsn_ctxt = ctxt;
    dsn->dsn_func = func;
    _dispatch_retain(dq);
    dispatch_atomic_store_barrier();
    //将结构体放到链表尾部,如果链表为空同时设置链表头部节点并唤醒group
    prev = dispatch_atomic_xchg2o(dsema, dsema_notify_tail, dsn);
    if (fastpath(prev)) {
        prev->dsn_next = dsn;
    } else {
        _dispatch_retain(dg);
        (void)dispatch_atomic_xchg2o(dsema, dsema_notify_head, dsn);
        if (dsema->dsema_value == dsema->dsema_orig) {//任务已经完成,唤醒group
            _dispatch_group_wake(dsema);
        }
    }
}

所以 dispatch_group_notify 函数只是用链表把所有回调通知保存起来,等待调用。

_dispatch_group_wake源码

static long
_dispatch_group_wake(dispatch_semaphore_t dsema)
{
    struct dispatch_sema_notify_s *next, *head, *tail = NULL;
    long rval;
    //将dsema的dsema_notify_head赋值为NULL,同时将之前的内容赋给head
    head = dispatch_atomic_xchg2o(dsema, dsema_notify_head, NULL);
    if (head) {
        // snapshot before anything is notified/woken <rdar://problem/8554546>
        //将dsema的dsema_notify_tail赋值为NULL,同时将之前的内容赋给tail
        tail = dispatch_atomic_xchg2o(dsema, dsema_notify_tail, NULL);
    }
    //将dsema的dsema_group_waiters设置为0,并返回原来的值
    rval = dispatch_atomic_xchg2o(dsema, dsema_group_waiters, 0);
    if (rval) {
        //循环调用semaphore_signal唤醒当初等待group的信号量,使得dispatch_group_wait函数返回。
        // wake group waiters
#if USE_MACH_SEM
        _dispatch_semaphore_create_port(&dsema->dsema_waiter_port);
        do {
            kern_return_t kr = semaphore_signal(dsema->dsema_waiter_port);
            DISPATCH_SEMAPHORE_VERIFY_KR(kr);
        } while (--rval);
#elif USE_POSIX_SEM
        do {
            int ret = sem_post(&dsema->dsema_sem);
            DISPATCH_SEMAPHORE_VERIFY_RET(ret);
        } while (--rval);
#endif
    }
    if (head) {
        //获取链表,依次调用dispatch_async_f异步执行在notify函数中的任务即Block。
        // async group notify blocks
        do {
            dispatch_async_f(head->dsn_queue, head->dsn_ctxt, head->dsn_func);
            _dispatch_release(head->dsn_queue);
            next = fastpath(head->dsn_next);
            if (!next && head != tail) {
                while (!(next = fastpath(head->dsn_next))) {
                    _dispatch_hardware_pause();
                }
            }
            free(head);
        } while ((head = next));
        _dispatch_release(dsema);
    }
    return 0;
}

_dispatch_group_wake主要的作用有两个:

  1. 调用 semaphore_signal 唤醒当初等待group的信号量,使得 dispatch_group_wait 函数返回。

  2. 获取链表,依次调用 dispatch_async_f 异步执行在notify函数中的任务即Block。

到这里我们已经差不多知道了dispatch_group工作过程,我们用一张图表示:

dispatch_group.png

dispatch_group_wait源码

long
dispatch_group_wait(dispatch_group_t dg, dispatch_time_t timeout)
{
    dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;

    if (dsema->dsema_value == dsema->dsema_orig) {//没有需要执行的任务
        return 0;
    }
    if (timeout == 0) {//返回超时
#if USE_MACH_SEM
        return KERN_OPERATION_TIMED_OUT;
#elif USE_POSIX_SEM
        errno = ETIMEDOUT;
        return (-1);
#endif
    }
    return _dispatch_group_wait_slow(dsema, timeout);
}

dispatch_group_wait 用于等待group中的任务完成。

_dispatch_group_wait_slow源码

static long
_dispatch_group_wait_slow(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
    long orig;

again:
    // check before we cause another signal to be sent by incrementing
    // dsema->dsema_group_waiters
    if (dsema->dsema_value == dsema->dsema_orig) {
        return _dispatch_group_wake(dsema);
    }
    // Mach semaphores appear to sometimes spuriously wake up. Therefore,
    // we keep a parallel count of the number of times a Mach semaphore is
    // signaled (6880961).
    (void)dispatch_atomic_inc2o(dsema, dsema_group_waiters);
    // check the values again in case we need to wake any threads
    if (dsema->dsema_value == dsema->dsema_orig) {
        return _dispatch_group_wake(dsema);
    }

#if USE_MACH_SEM
    mach_timespec_t _timeout;
    kern_return_t kr;

    _dispatch_semaphore_create_port(&dsema->dsema_waiter_port);

    // From xnu/osfmk/kern/sync_sema.c:
    // wait_semaphore->count = -1; /* we don't keep an actual count */
    //
    // The code above does not match the documentation, and that fact is
    // not surprising. The documented semantics are clumsy to use in any
    // practical way. The above hack effectively tricks the rest of the
    // Mach semaphore logic to behave like the libdispatch algorithm.

    switch (timeout) {
    default:
        do {
            uint64_t nsec = _dispatch_timeout(timeout);
            _timeout.tv_sec = (typeof(_timeout.tv_sec))(nsec / NSEC_PER_SEC);
            _timeout.tv_nsec = (typeof(_timeout.tv_nsec))(nsec % NSEC_PER_SEC);
            kr = slowpath(semaphore_timedwait(dsema->dsema_waiter_port,
                    _timeout));
        } while (kr == KERN_ABORTED);

        if (kr != KERN_OPERATION_TIMED_OUT) {
            DISPATCH_SEMAPHORE_VERIFY_KR(kr);
            break;
        }
        // Fall through and try to undo the earlier change to
        // dsema->dsema_group_waiters
    case DISPATCH_TIME_NOW:
        while ((orig = dsema->dsema_group_waiters)) {
            if (dispatch_atomic_cmpxchg2o(dsema, dsema_group_waiters, orig,
                    orig - 1)) {
                return KERN_OPERATION_TIMED_OUT;
            }
        }
        // Another thread called semaphore_signal().
        // Fall through and drain the wakeup.
    case DISPATCH_TIME_FOREVER:
        do {
            kr = semaphore_wait(dsema->dsema_waiter_port);
        } while (kr == KERN_ABORTED);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
        break;
    }
#elif USE_POSIX_SEM
//这部分代码省略
#endif

    goto again;
}

从上面的代码我们发现 _dispatch_group_wait_slow_dispatch_semaphore_wait_slow 的逻辑很接近。都利用mach内核的semaphore进行信号的发送。区别在于 _dispatch_semaphore_wait_slow 在等待结束后是return,而 _dispatch_group_wait_slow 在等待结束是调用 _dispatch_group_wake 去唤醒这个group。

调度组相关函数源码解析总结

  1. dispatch_group 是一个初始值为 LONG_MAX 的信号量,group中的任务完成是判断其value是否恢复成初始值。

  2. dispatch_group_enterdispatch_group_leave 必须成对使用并且支持嵌套。

  3. 如果 dispatch_group_enterdispatch_group_leave 多,由于 value 不等于 dsema_orig 不会走到唤醒逻辑,dispatch_group_notify 中的任务无法执行或者 dispatch_group_wait 收不到信号而卡住线程。如果是 dispatch_group_leave 多,则会引起崩溃。

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

推荐阅读更多精彩内容