【iOS】GCD学习

【iOS】GCD学习

前言

GCD是iOS开发中非常重要的一个内容,在此作一个总结。

什么是GCD

GCD全称Grand Central Dispatch,是异步执行任务的技术之一 。一般将应用程序中记述的线程管理用的代码在系统及中实现。开发者只需要定义想执行的任务并追加到适当的调度队列中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可执行任务,这样会比以前的线程更有效率。

仅一行代码就能表示让处理在后台线程中执行。

那么为什么使用GCD?

  • GCD可用于多核的并行运算,即多任务在不同CPU核心上同时运行。
  • GCD会自动利用更多的CPU内核(比如双核、四核),即自动利用多个线程。
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

任务和队列

任务:指在线程中执行操作的代码。在GCD中是放在Block中的。执行任务的方式有两种:

  • 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力。

二者最主要的区别在于:是否具备开启新线程的能力。

队列:队列中允许插入操作的一端称为队尾,允许删除操作的一端称为队头。在GCD里队列是指执行任务的等待队列,用来存放任务。按照先进先出的特点,新任务总是插入在队列的末尾,而任务的执行总是从队列的队头输出。每读取一个任务,则从队列中释放一个任务。GCD的队列分为两种:

  • 串行队列:只开启一个线程,每次只能有一个任务执行,等待执行完毕后才会执行下一个任务。
  • 并发队列:可以对多个任务同时执行,也就是开启多个线程,让多个任务同时执行。

二者最主要的区别在于:执行顺序不同,开启线程数不同。

GCD基本使用

队列的创建

队列创建只需要调用dispatch_queue_create方法,该方法有两个参数:

  • const char * _Nullable label:表示队列的唯一标识,可以传空。
  • dispatch_queue_attr_t _Nullable attr:用来识别是串行队列还是并发队列。
objc 复制代码
//串行队列
dispatch_queue_t serialQueue = dispatch_queue_create(@"myGCD", DISPATCH_QUEUE_SERIAL);
//并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"myGCD", DISPATCH_QUEUE_CONCURRENT);

GCD默认一种全局并发队列,调用dispatch_get_global_queue方法来获取全局并发队列,该方法有两个参数:

  • intptr_t identifier:是一个长整型类型的参数,表示队列优先级,可用常量有:
    • DISPATCH_QUEUE_PRIORITY_DEFAULT:默认优先级,适用于普通任务。
    • DISPATCH_QUEUE_PRIORITY_HIGH:高优先级,适用于高速任务、重要任务。
    • DISPATCH_QUEUE_PRIORITY_LOW:低优先级,适用于不重要的后台任务。
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND:后台优先级,适用于日志写入、清理缓存等。
  • uintptr_t flags:写0即可。
objc 复制代码
//全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

GCD默认主队列,调用dispatch_get_main_queue方法获取,所有放在主队列中的任务都会在主线程中执行。主队列是一种串行队列

objc 复制代码
//主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

任务的创建

  • 异步任务:调用dispatch_async创建。
objc 复制代码
//异步任务
dispatch_async(concurrentQueue, ^{
    NSLog(@"异步执行的代码");
});
  • 同步任务:调用dispatch_sync创建。
objc 复制代码
//同步任务
dispatch_sync(concurrentQueue, ^{
    NSLog(@"同步执行的代码");
});

任务队列组合

在不考虑嵌套任务的情况下会存在以下几种情况:

我们通过[NSThread currentThread]获取当前线程对象的指针地址、线程编号和线程名字来测试一下。

  • 同步任务+串行队列:
objc 复制代码
-(void)syncTaskWithSerial {
  NSLog(@"%@", [NSThread currentThread]);
  dispatch_queue_t queue = dispatch_queue_create("syncTaskWithSerial", DISPATCH_QUEUE_SERIAL);
  dispatch_sync(queue, ^{
      NSLog(@"1:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"2:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"3:%@", [NSThread currentThread]);
  });
}

同步任务+串行队列:没有开启新线程,任务是按顺序执行的。

  • 同步任务+并发队列:
objc 复制代码
-(void)syncTaskWithConcurrent {
  NSLog(@"%@", [NSThread currentThread]);
  dispatch_queue_t queue = dispatch_queue_create("syncTaskWithConcurrent", DISPATCH_QUEUE_CONCURRENT);
  dispatch_sync(queue, ^{
      NSLog(@"1:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"2:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"3:%@", [NSThread currentThread]);
  });
}

同同步任务+串行队列一样,同步任务+并发队列:没有开启新线程,任务是按顺序执行的。

由此可见,并发队列能并发的前提是任务必须是异步派发。sync会强制任务在当前线程中执行,导致并发能力被禁用。

  • 异步任务+串行队列:
objc 复制代码
-(void)asyncTaskWithSerial {
  NSLog(@"%@", [NSThread currentThread]);
  dispatch_queue_t queue = dispatch_queue_create("asyncTaskWithSerial", DISPATCH_QUEUE_SERIAL);
  dispatch_async(queue, ^{
      NSLog(@"1:%@", [NSThread currentThread]);
  });
  dispatch_async(queue, ^{
      NSLog(@"2:%@", [NSThread currentThread]);
  });
  dispatch_async(queue, ^{
      NSLog(@"3:%@", [NSThread currentThread]);
  });
  NSLog(@"继续执行后续代码");
}

异步任务+串行队列:开启了一个新线程,说明异步任务具备开启新线程的能力。但是由于任务是在串行队列中执行,因此任务是按顺序执行的。

输出"继续执行后续代码":因为是异步,所以不会阻塞当前线程,这意味着在我们创建的这个队列中执行了三个块,这三个块将被提交到队列中,而当前线程会继续执行后面代码。又由于是串行队列,它们将依照提交顺序依次执行。

  • 异步任务+并发队列:
objc 复制代码
-(void)asyncTaskWithConcurrent {
    NSLog(@"%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("asyncTaskWithConcurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3:%@", [NSThread currentThread]);
    });
    NSLog(@"继续执行后续代码");
}

异步任务+并发队列:生成多个线程,并且任务是并发执行的,即多个任务按照随机顺序同时执行。

  • 主队列+同步任务:
objc 复制代码
-(void)syncTaskWithMain {
  NSLog(@"%@", [NSThread currentThread]);
  dispatch_queue_t queue = dispatch_get_main_queue();
  dispatch_sync(queue, ^{
      NSLog(@"1:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"2:%@", [NSThread currentThread]);
  });
  dispatch_sync(queue, ^{
      NSLog(@"3:%@", [NSThread currentThread]);
  });
  NSLog(@"继续执行后续代码");
}

我们会发现出现了错误。这是因为GCD的主队列底层就是一个串行队列。dispatch_get_main_queue会把任务放到主线程的队列中,而同步执行会使得主线程正在执行我们的方法,我们又让主线程同步等待一个主队列里的任务,即必须等待这个Block执行完,才能继续往下执行,而主队列里的任务又必须等待主线程空出来才能执行 。于是相互等待,就会产生死锁的情况。

  • 主队列+异步任务
objc 复制代码
-(void)asyncTaskWithMain {
  NSLog(@"%@", [NSThread currentThread]);
  dispatch_queue_t queue = dispatch_get_main_queue();
  dispatch_async(queue, ^{
      NSLog(@"1:%@", [NSThread currentThread]);
  });
  dispatch_async(queue, ^{
      NSLog(@"2:%@", [NSThread currentThread]);
  });
  dispatch_async(queue, ^{
      NSLog(@"3:%@", [NSThread currentThread]);
  });
  NSLog(@"继续执行后续代码");
}

主队列+异步任务:由于主队列是串行队列,因此虽然是异步任务,但是并没有开启新线程,仍然是在主线程中,并且按照顺序执行的。

GCD相关方法

dispatch_set_target_queue

使用的两个目的:

  • 改变队列的优先级
  • 建立队列层级关系,防止并行执行

dispatch_queue_create 方法生成的队列不论是串行还是并发,都和全局并发队列globalQueue的默认优先级相同。对于变更优先级,我们需要使用dispatch_set_target_queue方法。

objc 复制代码
dispatch_set_target_queue(<dispatch_object_t  _Nonnull object>, <dispatch_queue_t  _Nullable queue>);

参数1:目标队列,通常是创建的自定义队列(串行或并发),这个队列的属性将被改变。

参数2:参考队列,通常是全局队列或其他自定义队列,作为优先级和执行特性的参考标准,该队列的属性将被复制到目标队列。

dispatch_semaphore

是GCD提供的信号量同步机制,基于计数器实现线程间的同步、并发控制和资源保护,核心作用是通过控制"可用资源数"来协调多线程的执行节奏。

信号量:是多线程同步的核心工具。

  • 用一个整数计数器表示当前可用资源数量。
  • 线程想要使用资源时,先"申请"(计数器-1),若资源不足则排队等待。
  • 线程用完资源后,"释放"(计数器+1),唤醒排队的线程继续执行。

用一个很形象的比喻来理解:

  • 信号初始值:停车场总车位数,比如初始值为3,即有3个车位,3个可用资源。
  • dispatch_semaphore_wait:车辆进停车场,车位-1。若车位>0,直接进;若<1,车辆排队。
  • dispatch_semaphore_signal:车辆出停车场,车位+1,释放1个车位,唤醒排队的1辆车进入。
  • 线程:车辆。线程竞争资源(车位),信号量协调执行顺序。
objc 复制代码
dispatch_semaphore_t currSingal = dispatch_semaphore_create(value);//初始化信号量,小于0返回NULL
dispatch_semaphore_signal(dispatch_semaphore_t  _Nonnull dsema);//发送信号量让信号量加1
dispatch_semaphore_wait(dispatch_semaphore_t  _Nonnull dsema, dispatch_time_t timeout);//释放信号量,让总信号量减1,信号量小于0的时候就会一直等待,否则直接执行

这样使用的目的:

  • 保持线程同步,将异步任务转化为同步执行任务。
  • 保障线程的安全,为线程加锁。

核心应用:

  1. 并发队列里的异步任务顺序执行
objc 复制代码
dispatch_semaphore_t currentSingal = dispatch_semaphore_create(0);
dispatch_queue_t que = dispatch_get_global_queue(0, 0);
dispatch_async(que, ^{
    NSLog(@"执行任务1:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(currentSingal);
});
dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);
dispatch_async(que, ^{
    NSLog(@"执行任务2:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(currentSingal);
});
dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);
dispatch_async(que, ^{
    NSLog(@"执行任务3:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(currentSingal);
});

可以看的出,我们 dispatch_semaphore 的等待唤醒机制现在让并发队列中的异步任务(默认行为是任务被分发到不同线程并执行),从本该并行执行强制转换为严格按照任务1、任务2、任务3的顺序串行执行。

  1. 主线程等待异步任务完成(线程同步)
objc 复制代码
-(void)semaphoreSync {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"semaphore开始");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;//允许在异步block中修改
    //异步派发任务到全局并发队列,任务在子线程执行,不阻塞当前线程
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];//任务1:模拟耗时操作,如网络请求、文件读写等
        NSLog(@"1---%@",[NSThread currentThread]);
        number = 100;
        dispatch_semaphore_signal(semaphore);//发送信号量(+1),如果有线程在wait阻塞,会被唤醒
    });
    //等待信号量(-1),信号量变为-1,当前线程阻塞
    //DISPATCH_TIME_FOREVER:表示永久等待,直到收到singal
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore结束,number = %d",number);
}

从结果我们可以看出:

打印结果可以拿到异步任务修改后的结果的原因是使用了dispatch_semaphore_wait。

  • 信号量使得主线程派发异步任务后,调用wait,阻塞线程,等待子线程。
  • 与此同时,子线程执行异步任务(sleep2秒),修改number为100,然后子线程发现线程在wait阻塞,被唤醒发信号
  • 主线程收到信号,继续往下执行,打印number(此时number已被修改)。

这样就实现了线程同步,将异步执行任务转换为同步执行。

  1. 控制最大并发数

本质是资源限流,避免并发线程过多而导致的内存/CPU过大。

objc 复制代码
dispatch_semaphore_t currentSingal = dispatch_semaphore_create(3);//最多同时允许3个任务执行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
  dispatch_async(queue, ^{
    dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务:%d", i);
    sleep(1);
    NSLog(@"完成任务:%d", i);
    dispatch_semaphore_signal(currentSingal);
  });
}

这里我们捋一下关键执行的逻辑:

每次最多同时允许3个任务执行。

  • 第一批:信号量初始化为3,三个任务的wait依次将信号量减到0,无阻塞,所以同时执行,三个任务依次完成后,信号量加回到3。
  • 第二批:i = 3、4、5的三个任务的wait依次减到0,无阻塞,直接执行,3个子线程同时执行 sleep(1) ,导致线程差异,导致其中某一任务(随机的)最先完成sleep。
  • 后续任务:信号量的实时补位规则,i为4的任务完成后,信号量加1,此时阻塞队列中的任务6被唤醒,执行wait,信号量减为0,任务6开始执行。而任务3/5还在sleep,直到它们完成后,singal才会继续释放名额,唤醒任务7/8依次类推。

其中触发了优先级反转的警告。

优先级反转指的是高优先级任务因为资源被低优先级任务占用,结果被迫等待。而一个中等优先级任务却能先运行,导致优先级顺序被"反转"。

那么为什么会发生优先级反转呢?

假设有三个任务A、B、C,优先级分别是最高优先级、中、最低。任务C运行并持有一把锁,然后A优先级高于C,抢占了C开始运行。任务A也像申请那把锁,但锁被C持有,A被阻塞。系统中现可运行的任务是B和C,根据优先级,系统先调度任务B(中优先级)运行。这会导致任务C,也就是持锁者迟迟等不到机会来释放锁,任务A被C阻塞,中间的任务却先运行,形成了"优先级反转"。

那么为什么iOS中容易发生优先级反转呢?

关键原因来自信号量是公平队列FIFO。

  • 信号量等待队列是FIFO,不按优先级:等待的线程进入顺序排队,被唤醒时不考虑优先级(QoS)。
  • 系统调度是基于优先级的:高优先级线程调度更快,但如果它在等待信号量,仍然会被阻塞。

问题在于两套机制不一致。

总结一下,持锁者低优先级、竞争锁者高优先级,有一个中间优先级的任务不断占用CPU,调度+锁机制冲突共同导致低优先级任务影响高优先级,使高优先级被反转到更低级别,即优先级反转。所以,我们要尽量减少使用信号量做锁,避免iOS中出现优先级反转问题。

  1. 线程安全和线程同步(为线程加锁)

在多线程环境下,如果多个线程同时访问并修改同一份数据,就会出现线程不安全的问题;而当程序之间需要某种顺序协作时,就涉及线程同步。Dispatch Semaphore 是一个能同时解决这两个问题的工具,既能加锁保护数据共享数据(线程安全),也能控制线程等待(线程同步)

这里我们用一个例子------售票问题来展示 Dispatch Semaphore 实现线程安全和线程同步:

objc 复制代码
-(instancetype)init {
    if (self = [super init]) {
        self.ticketCount = 20;
        self.ticketLock = dispatch_semaphore_create(1);
    }
    return self;
}

-(void)saleTicketSafe {
    while (1) {
        //加锁:信号量<=0进入阻塞等待
        dispatch_semaphore_wait(self.ticketLock, DISPATCH_TIME_FOREVER);
        if (self.ticketCount > 0) {
            self.ticketCount--;
            NSLog(@"剩余票数:%ld,窗口:%@", (long)self.ticketCount, [NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.2];
        } else {
            NSLog(@"全部售完");
            dispatch_semaphore_signal(self.ticketLock);
            break;
        }
        //解锁:信号量+1,唤醒下一个线程
        dispatch_semaphore_signal(self.ticketLock);
    }
}
objc 复制代码
self.manager = [[TicketManager alloc] init];
//开两个线程模拟两个售票窗口
NSThread *windowA = [[NSThread alloc] initWithTarget:self.manager selector:@selector(saleTicketSafe) object:nil];
NSThread *windowB = [[NSThread alloc] initWithTarget:self.manager selector:@selector(saleTicketSafe) object:nil];
windowA.name = @"窗口A";
windowB.name = @"窗口B";
//启动线程
[windowA start];
[windowB start];

从结果可以看出,我们实现了两个线程没有同时操作,且两个线程按顺序轮流执行,实现了线程同步;票数没有负数,说明锁保护了共享变量,实现了线程安全。

dispatch_after

dispatch_after表示延迟把任务加入队列中,而不是延迟执行。

objc 复制代码
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_after 的 block 被加入主队列");
});
//故意让主线程忙2秒
NSLog(@"主线程忙 2 秒...");
[NSThread sleepForTimeInterval:2];//阻塞主线程
NSLog(@"主线程忙完了");

dispatch_once

保证在整个App生命周期只执行一次block代码块中的代码,常用于单例。

objc 复制代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"创建单例");
});

dispatch_apply

按照指定的次数将指定的任务追加到指定的队列中,快速迭代一组任务,并等待全部队列执行结束。通俗地说,就是多线程版本的for循环。主要用于需要快速并行处理大量相同类型任务的场景,如图像处理、数据处理或任何形式的批处理任务。

  • 在串行队列中:队列中任务始终是一次执行一个,按顺序执行。
  • 在并行队列/全局队列中:多线程并发执行,作用最大。
objc 复制代码
//参数1:重复次数
//参数2:追加的队列
//参数3:执行任务
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"开始计算%@", [NSThread currentThread]);
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
dispatch_apply(10, queue, ^(size_t iteration) {
    NSLog(@"计算在线程%@", [NSThread currentThread]);
    sleep(1);
});
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"计算完成,耗时%.2f秒", end - start);
objc 复制代码
NSLog(@"开始计算%@", [NSThread currentThread]);
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();//计算代码执行耗时
for (int i = 0; i < 10; i++) {
    NSLog(@"计算在线程%@", [NSThread currentThread]);
    sleep(1);
}
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"计算完成,耗时%.2f秒", end - start);

通过对比,我们可以看出 dispatch_apply 对并行队列是最有效的,极大的提高了效率,节约了时间。

  • 在主队列中:死锁。

dispatch_group_t

用于监听一组异步任务何时完成。常用于以下几个场景:

  • 多个网络请求全部完成后刷新UI
  • 批量图片下载,全部完成后统一处理

dispatch_group_async + dispatch_group_notify

  • async:异步完成后再回调,不会阻塞线程。
  • notify:作用在于当group中所有任务完成后再触发执行block。

最常用且最适合多个请求完成后刷新UI。

objc 复制代码
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
    NSLog(@"请求1完成");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"请求2完成");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"刷新界面");
});

dispatch_group_enter / leave + dispatch_group_notify

当我们不方便使用 dispatch_group_async 时(例如 URLSession回调、AFNetworking、多层嵌套异步等),就需要使用 dispatch_group_enter / leave 手动管理。

解释以上情况为什么不适合使用 dispatch_group_async。

首先我们知道 dispatch_group_async + dispatch_group_notify 的本质是我们自己把任务block提交到GCD队列中执行,block结束后,group自动 leave,当所有任务完成,notify 回调。也就是说,只有由GCD本身执行的同步区块,才能自动让group计数正常。那么看这几种情况:

  • URLSession 回调:URLSession 的异步回调不在我们的 block 结束时执行。
objc 复制代码
dispatch_group_async(group, queue, ^{
  NSURLSessionDataTask *task = [session dataTaskWithURL:url];
  [task resume];
});

看起来我们确实把任务加入到了group,但实际上任务并没有在这个block内完成。真正的执行顺序是block执行到[task resume];时,让group以为它已经完成了,然而真正的网络请求在后台线程跑,很久之后才回调到 completionHandler,因此不适合。

  • 第三方库AFNetworking:与URLSession 回调一样,也是异步回调,请求方法会立即返回,并不会等网络请求完成。
  • 多层嵌套异步:同样,block只执行了最外层API的调用,让group误以为以为已完成任务,但实际上是没有完全完成。
objc 复制代码
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
  NSLog(@"请求1完成");
  dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
  NSLog(@"请求 2 完成");
  dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
  NSLog(@"刷新界面");
});
  • enter:表示"我有任务要执行了"
  • leave:表示"任务执行完了"

enter必须与leave配对执行,否则group永远不会结束。

dispatch_group_wait

wait 会阻塞当前线程,知道任务完成或超时。

objc 复制代码
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
  sleep(2);
  NSLog(@"任务完成");
  dispatch_group_leave(group);
});
NSLog(@"开始等待");
//group:需要等待的调度组
//timeout:等待的超时时间
//等待方式:
//DISPATCH_TIME_NOW:意味着不等待直接判定调度组是否执行完毕
//DISPATCH_TIME_FOREVER:会阻塞当前调度组,直到调度组执行完毕
//自定义时间:等待若干秒
//返回值:
//返回值为0:在指定时间内调度组完成了任务
//返回值不为0:在指定时间内调度组没有按时完成任务
long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (timeout == 0) {
  NSLog(@"按时完成任务");
} else {
  NSLog(@"超时");
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
  NSLog(@"刷新界面");
});

总结一下notify和wait的区别:

  • dispatch_group_notify:不阻塞线程,多任务完成后刷新UI。
  • dispatch_group_wait:阻塞线程,必须等待结果才能继续执行。

dispatch_barrier_async

dispatch_barrier 是GCD中的栅栏函数。用于在并发队列上,插入一个"独占执行"的任务,使它前后任务不会并发执行。通俗地说,栅栏前的任务必须全部完成,栅栏任务自己必须独占执行(不会并发),栅栏后的任务必须等栅栏执行完才开始。这可以用于确保在多线程中某些操作的顺序性和互斥性。

  • dispatch_barrier_async:可以控制队列中任务的执行顺序,前面的任务执行完毕后才会来这里。
  • dispatch_barrier_sync:作用相同,但是同步任务,sync要求当前线程必须等它执行完才能继续,这样不仅阻塞了队列的执行,也阻塞了线程的执行,影响后面任务执行。

dispatch_barrier_async 主要有两种使用场景:串行队列、并发队列。

  1. 串行队列:
objc 复制代码
-(void)serialBarrier {
    dispatch_queue_t queue = dispatch_queue_create("serialBarrier", DISPATCH_QUEUE_SERIAL);
    NSLog(@"开始:%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2秒的任务1:%@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束:%@", [NSThread currentThread]);
    //栅栏函数将队列中的任务进行分组
    dispatch_barrier_async(queue, ^{
        NSLog(@"栅栏任务:%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2秒的任务2:%@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束:%@", [NSThread currentThread]);
}

因为串行任务本来就是一次只执行一个任务,不存在并发,所以看上去栅栏没有起到特殊作用。

  1. 并发队列:
objc 复制代码
-(void)concurrentBarrier {
    dispatch_queue_t queue = dispatch_queue_create(@"concurrentBarrier", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"开始:%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2秒的任务1:%@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束:%@", [NSThread currentThread]);
    //栅栏函数将队列中的任务进行分组
    dispatch_barrier_async(queue, ^{
        NSLog(@"栅栏任务:%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2秒的任务2:%@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束:%@", [NSThread currentThread]);
}

这样我们发现并发队列本应该在多线程同时执行,因为栅栏函数在延迟2秒的任务1执行时,挡住了下面的任务,这样就很好的控制了队列内任务执行的顺序。

barrier 最典型的用途是并发读、独占写,即读写锁。

objc 复制代码
dispatch_queue_t rwQueue = dispatch_queue_create(@"rwQueue", DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *array = [NSMutableArray array];
//写操作:用barrier独占执行
dispatch_barrier_async(rwQueue, ^{
  [array addObject:@"数据"];
  NSLog(@"写入完成");
});
//读操作:可以并发执行
dispatch_async(rwQueue, ^{
  NSLog(@"读取任务1:%@", array);
});
dispatch_async(rwQueue, ^{
  NSLog(@"读取任务2:%@", array);
});

这样读操作可以并发,提高性能;写操作被barrier独占,保证数据安全。

dispatch_source_t

dispatch_source_t 是GCD中最底层、最强大的事件处理机制之一,负责监听系统底层事件。本质上是一个"事件源",当某个事件发生时,它会将事件丢到我们指定的队列上执行回调。它可以用来监听底层事件(如文件变化、计时器、进程、网络等),主要用于计时操作。相比NSTimer、KVO、通知等,dispatch_source_t 不依赖RunLoop,精度更高。

在iOS开发中,一般使用NSTimer来处理定时逻辑,但NSTimer依赖RunLoop,且默认只在指定模式下触发。如果模式切换时,NSTimer就会挂机;又如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周期。因此NSTimer在计时上会有误差,并不是特别精确。

objc 复制代码
-(void)testSource {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //dispatch_source是一种基本数据类型,用来监听一些底层系统事件
    //DISPATCH_SOURCE_TYPE_TIMER:定时器事件源,用来生成周期性的通知或回调
    //DISPATCH_SOURCE_TYPE_SIGNAL:监听信号事件源,当有UNIX信号发生时会通知
    //UNIX信号:是UNIX/Linux系统(包括macOS/iOS)中用于进程间通信和系统向进程发送事件通知的一种机制,是操作系统与进程、进程与进程之间传递"紧急事件"的核心方式
    //DISPATCH_SOURCE_TYPE_VNODE:监听文件事件源,当文件数据发生变化时会通知
    //DISPATCH_SOURCE_TYPE_PROC:监听进程事件源,与进程相关的事件通知
    //Mach:监听Mach端口事件源
    //Mach端口:是Mach内核为进程/线程/内核组件之间传递消息(数据、指令、事件)设计的"通信接口",是iOS/macOS底层通信的基石
    //Custom:监听自定义事件源
    
    //常用API:
    //dispatch_source_create:创建事件源
    //dispatch_source_set_event_handler:设置数据源回调
    //dispatch_source_merge_data:设置事件源数据
    //dispatch_source_get_data:获取事件源数据
    //dispatch_resume:继续
    //dispatch_suspend:挂起
    //dispatch_cancle:取消
    
    //创建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置timer首次执行时间、间隔、精确度
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
    //设置timer事件回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"testSource");
    });
    //默认挂起状态,需要手动激活
    dispatch_activate(timer);
}

disoatch_suspend & dispatch_resume

objc 复制代码
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_suspend(queue);//把队列挂起,暂停调度
dispatch_resume(queue);//恢复队列

总结

以上就笔者关于GCD总结的相关内容,后续学习会继续完善。

相关推荐
蜀中廖化3 小时前
VLA技术调研及学习
学习
m0_578267863 小时前
下载数据集1.snaphic---mES bulk hic数据
学习·生信
测试19983 小时前
如何学习自动化测试?
自动化测试·软件测试·python·学习·测试工具·职场和发展·测试用例
●VON3 小时前
从单端到“空地一体”:基于 HarmonyOS 的多端协同感知系统开发实践
学习·华为·harmonyos·openharmony·开源鸿蒙
Kathleen1004 小时前
iOS--TableView的复用机制以及性能优化(处理网络数据)
ios·性能优化·网络请求·gcd·uitableview
眼眸流转4 小时前
Godot学习笔记
笔记·学习·godot
圆弧YH4 小时前
键盘→语言操作
学习
世界宇宙超级无敌究极特级顶级第一非常谱尼4 小时前
RF Power Amplifers for Wireless Communications 第一章学习笔记
笔记·学习·pa·功率放大器·mmic
dog2504 小时前
让算法去学习,而不是去启发
学习·算法