iOS之深入分析GCD的函数与队列以及多种组合使用

一、GCD 简介
① 什么是 GCD ?
  • GCD 是 Apple 开发的一个多核编程的较新的解决方法;
  • GCD 全称:Grand Central Dispatch,是纯 C 语言,提供非常多强大的函数;
  • 它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统;
  • 它是一个在线程池模式的基础上执行的并发任务;
  • 在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。
  • 简而言之,GCD 就是将任务添加到队列,并指定任务执行的函数
② GCD 优势
  • GCD 是苹果公司为多核的并行运算提出的解决方案;
  • GCD 会自动利用更多的CPU内核(比如双核、四核);
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
二、任务与队列
① 任务
  • 任务就是执行操作的意思,换句话说就是在线程中执行的那段代码,在 GCD 中是放在 block 中的。
  • 执行任务有两种方式:『同步执行』 和 『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
  • 同步执行(sync):
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行;
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async):
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务;
    • 可以在新的线程中执行任务,具备开启新线程的能力。
  • 任务的创建:GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。
	// 同步执行任务创建方法
	dispatch_sync(queue, ^{
	    // 同步执行任务代码
	});
	
	// 异步执行任务创建方法
	dispatch_async(queue, ^{
	    // 异步执行任务代码
	});
② 队列
  • 队列指执行任务的等待队列,即用来存放任务的队列。
  • 队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
  • 队列的结构可参考下图:

在这里插入图片描述

  • 在 GCD 中有两种队列:『串行队列』 和 『并发队列』,两者都符合 FIFO(先进先出)的原则,主要区别是:执行顺序不同,以及开启线程数不同。
  • 串行队列(Serial Dispatch Queue):每次只有一个任务被执行,让任务一个接着一个地执行(只开启一个线程,一个任务执行完毕后,再执行下一个任务),如下:

在这里插入图片描述

  • 使用dispatch_queue_create(“xx”, DISPATCH_QUEUE_SERIAL)创建串行队列,其中的 DISPATCH_QUEUE_SERIAL 也可以使用 NULL 表示,这两种均表示默认的串行队列;
	// 串行队列的获取方法
    dispatch_queue_t serialQueue1 = dispatch_queue_create("com.YDW.Queue", NULL);
    
    dispatch_queue_t serialQueue2 = dispatch_queue_create("com.YDW.Queue", DISPATCH_QUEUE_SERIAL);
  • 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(可以开启多个线程,并且同时执行任务),如下:

在这里插入图片描述

  • 使用 dispatch_queue_create(“xxx”, DISPATCH_QUEUE_CONCURRENT) 创建并发队列:
	// 并发队列的获取方法
	dispatch_queue_t concurrentQueue = dispatch_queue_create("com.YDW.Queue", DISPATCH_QUEUE_CONCURRENT);
  • 注意:并发队列的并发功能只有在异步函数(dispatch_async)下才有效。
③ 主队列和全局并发队列
  • 主队列(Main Dispatch Queue):GCD 中提供的特殊的串行队列:
    • 主要用来在主线程上调度任务的串行队列,依赖于主线程、主 Runloop,在 main 函数调用之前自动创建;
    • 不会开启线程;
    • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度;
    • 使用dispatch_get_main_queue()获得主队列;
    • 通常在返回主线程 更新UI时使用;
    • 注意:主队列其实并不特殊,实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。
	// 主队列获取方法
	dispatch_queue_t mainQueue = dispatch_get_main_queue();
  • 全局并发队列(Global Dispatch Queue):GCD 提供的默认并发队列:
    • 为了方便程序员的使用,苹果提供了全局队列dispatch_get_global_queue;
    • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列;
    • 使用 dispatch_get_global_queue 获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0);
    • 全局队列是一个并发队列;
	// 全局并发队列的获取方法
	// 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代;
	// 第二个参数暂时没用,用 0 即可
	dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
	
	// 优先级从高到低(对应的服务质量)依次为
	- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
	- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
	- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
	- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND
  • 日常开发中,全局并发队列与主队列通常配合使用:
	// 主队列 + 全局并发队列的日常使用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 执行耗时操作
        dispatch_async(dispatch_get_main_queue(), ^{
            // 回到主线程进行UI操作
        });
    });
三、函数与队列的组合
(一)组合方式
  • 既然有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么就有了四种不同的组合方式,这四种不同的组合方式是:
	同步执行 + 串行队列
	异步执行 + 串行队列
	同步执行 + 并发队列
	异步执行 + 并发队列
  • 实际上,上面还提到了两种默认队列:全局并发队列和主队列。全局并发队列可以作为普通并发队列来使用,但是当前代码默认放在主队列中,所以就又多了两种组合方式,这样就有六种不同的组合方式:
	同步执行 + 主队列
	异步执行 + 主队列
① 同步执行 + 串行队列
  • 任务按顺序执行,即任务一个接一个的在当前线程执行,不会开辟新线程:
	dispatch_queue_t serialQueue = dispatch_queue_create("com.YDW.queue", NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_sync(serialQueue, ^{
            NSLog(@"同步执行 + 串行队列 : %d-%@", i, [NSThread currentThread]);
        });
    }
  • 执行结果如下:
	2021-03-24 20:33:25.553587+0800 GCD[48756:7803380] 同步执行 + 串行队列 : 0-<NSThread: 0x600003f3cac0>{number = 1, name = main}
	2021-03-24 20:33:25.553810+0800 GCD[48756:7803380] 同步执行 + 串行队列 : 1-<NSThread: 0x600003f3cac0>{number = 1, name = main}
	2021-03-24 20:33:25.553893+0800 GCD[48756:7803380] 同步执行 + 串行队列 : 2-<NSThread: 0x600003f3cac0>{number = 1, name = main}
	2021-03-24 20:33:25.553984+0800 GCD[48756:7803380] 同步执行 + 串行队列 : 3-<NSThread: 0x600003f3cac0>{number = 1, name = main}
	2021-03-24 20:33:25.554072+0800 GCD[48756:7803380] 同步执行 + 串行队列 : 4-<NSThread: 0x600003f3cac0>{number = 1, name = main}
② 异步执行 + 串行队列
  • 任务按顺序执行,即任务一个接一个的执行,但会开辟新线程:
	dispatch_queue_t serialQueue = dispatch_queue_create("com.YDW.queue", NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(serialQueue, ^{
            NSLog(@"异步执行 + 串行队列 : %d-%@", i, [NSThread currentThread]);
        });
    }
  • 执行结果如下:
	2021-03-24 20:35:18.365089+0800 GCD[48776:7805999] 异步执行 + 串行队列 : 0-<NSThread: 0x60000013a100>{number = 8, name = (null)}
	2021-03-24 20:35:18.365240+0800 GCD[48776:7805999] 异步执行 + 串行队列 : 1-<NSThread: 0x60000013a100>{number = 8, name = (null)}
	2021-03-24 20:35:18.365366+0800 GCD[48776:7805999] 异步执行 + 串行队列 : 2-<NSThread: 0x60000013a100>{number = 8, name = (null)}
	2021-03-24 20:35:18.365491+0800 GCD[48776:7805999] 异步执行 + 串行队列 : 3-<NSThread: 0x60000013a100>{number = 8, name = (null)}
	2021-03-24 20:35:18.365726+0800 GCD[48776:7805999] 异步执行 + 串行队列 : 4-<NSThread: 0x60000013a100>{number = 8, name = (null)}
③ 同步执行 + 并发队列
  • 任务按顺序执行,即任务一个接一个的执行,不开辟线程:
	dispatch_queue_t concurrentQueue = dispatch_queue_create("com.YDW.queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"同步执行 + 并发队列 : %d-%@", i, [NSThread currentThread]);
        });
    }
  • 执行结果如下:
	2021-03-24 20:39:46.185808+0800 GCD[48809:7810771] 同步执行 + 并发队列 : 0-<NSThread: 0x600003388240>{number = 1, name = main}
	2021-03-24 20:39:46.185940+0800 GCD[48809:7810771] 同步执行 + 并发队列 : 1-<NSThread: 0x600003388240>{number = 1, name = main}
	2021-03-24 20:39:46.186041+0800 GCD[48809:7810771] 同步执行 + 并发队列 : 2-<NSThread: 0x600003388240>{number = 1, name = main}
	2021-03-24 20:39:46.186150+0800 GCD[48809:7810771] 同步执行 + 并发队列 : 3-<NSThread: 0x600003388240>{number = 1, name = main}
	2021-03-24 20:39:46.186258+0800 GCD[48809:7810771] 同步执行 + 并发队列 : 4-<NSThread: 0x600003388240>{number = 1, name = main}
④ 异步执行 + 并发队列
  • 任务无序执行,即任务执行无顺序,会开辟新线程:
	dispatch_queue_t concurrentQueue = dispatch_queue_create("com.YDW.queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_async(concurrentQueue, ^{
            NSLog(@"异步执行 + 并发队列 : %d-%@", i, [NSThread currentThread]);
        });
    }
  • 执行结果如下:
	2021-03-24 20:42:55.252746+0800 GCD[48847:7815248] 异步执行 + 并发队列 : 2-<NSThread: 0x600002f6a980>{number = 5, name = (null)}
	2021-03-24 20:42:55.252747+0800 GCD[48847:7815249] 异步执行 + 并发队列 : 1-<NSThread: 0x600002f74240>{number = 4, name = (null)}
	2021-03-24 20:42:55.252750+0800 GCD[48847:7815256] 异步执行 + 并发队列 : 4-<NSThread: 0x600002f6c140>{number = 7, name = (null)}
	2021-03-24 20:42:55.252750+0800 GCD[48847:7815251] 异步执行 + 并发队列 : 0-<NSThread: 0x600002f1ac00>{number = 3, name = (null)}
	2021-03-24 20:42:55.252756+0800 GCD[48847:7815250] 异步执行 + 并发队列 : 3-<NSThread: 0x600002f74180>{number = 6, name = (null)}
⑤ 同步执行 + 主队列
  • 任务相互等待,造成死锁:
	NSLog(@"同步执行 + 主队列 : %@", [NSThread currentThread]);
	dispatch_queue_t mainQueue = dispatch_get_main_queue();
    for (int i = 0; i < 5; i++) {
        dispatch_sync(mainQueue, ^{
            NSLog(@"同步执行 + 主队列 : %d-%@", i, [NSThread currentThread]);
        });
	}
  • 造成死锁的原因分析:
    • 主队列有两个任务,顺序为:NSLog 任务 - 同步 block;
    • 执行 NSLog 任务后,执行同步 Block,会将任务1(即i=1时)加入到主队列,主队列顺序为:NSLog任务 - 同步block - 任务1;
    • 任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃。
⑥ 异步执行 + 主队列
  • 任务按顺序执行,即为任务一个接一个的执行,不开辟线程:
	dispatch_queue_t mainQueue = dispatch_get_main_queue();
    for (int i = 0; i < 5; i++) {
        dispatch_async(mainQueue, ^{
            NSLog(@"异步执行 + 主队列 : %d-%@", i, [NSThread currentThread]);
        });
    }
  • 执行结果如下:
	2021-03-24 20:50:18.098874+0800 GCD[48915:7823351] 异步执行 + 主队列 : 0-<NSThread: 0x600003530300>{number = 1, name = main}
	2021-03-24 20:50:18.099025+0800 GCD[48915:7823351] 异步执行 + 主队列 : 1-<NSThread: 0x600003530300>{number = 1, name = main}
	2021-03-24 20:50:18.099132+0800 GCD[48915:7823351] 异步执行 + 主队列 : 2-<NSThread: 0x600003530300>{number = 1, name = main}
	2021-03-24 20:50:18.099232+0800 GCD[48915:7823351] 异步执行 + 主队列 : 3-<NSThread: 0x600003530300>{number = 1, name = main}
	2021-03-24 20:50:18.099338+0800 GCD[48915:7823351] 异步执行 + 主队列 : 4-<NSThread: 0x600003530300>{number = 1, name = main}
(二)组合区别
① 主线程
  • 主线程中,不同队列 +不同任务简单组合的区别:
区别并发队列串行队列主队列
同步没有开启新线程,串行执行任务没有开启新线程,串行执行任务死锁卡住不执行
异步有开启新线程,并发执行任务有开启新线程(1条),串行执行任务没有开启新线程,串行执行任务
  • 从上边可看出: 『主线程』 中调用 『主队列』+『同步执行』 会导致死锁问题:
    • 这是因为主队列中追加的同步任务和主线程本身的任务两者之间相互等待,阻塞了 主队列,最终造成了主队列所在的线程(主线程)死锁问题;
    • 而如果我们在其他线程调用主队列 + 同步执行,则不会阻塞主队列,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务。
② 队列
  • 不同队列 + 不同任务组合,以及队列中嵌套队列使用的区别:
区别异步执行+并发队列嵌套同一个并发队列同步执行+并发队列嵌套同一个并发队列异步执行+串行队列嵌套同一个串行队列同步执行+串行队列嵌套同一个串行队列
同步(sync)没有开启新的线程,串行执行任务没有开启新线程,串行执行任务死锁卡住不执行死锁卡住不执行
异步(async)有开启新线程,并发执行任务有开启新线程,并发执行任务有开启新线程(1 条),串行执行任务有开启新线程(1 条),串行执行任务
  • 除了上边提到的主线程中调用主队列 + 同步执行会导致死锁问题。实际在使用串行队列的时候,也可能出现阻塞串行队列所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。
  • 下面代码这样:在异步执行 + 串行任务的任务中,又嵌套了当前的串行队列,然后进行同步执行,如下:
	dispatch_queue_t queue = dispatch_queue_create("com.YDW.queue", DISPATCH_QUEUE_SERIAL);
	dispatch_async(queue, ^{    // 异步执行 + 串行队列
	    dispatch_sync(queue, ^{  // 同步执行 + 当前串行队列
	        // 追加任务 1
	        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
	        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
	    });
	});
  • 执行上面的代码会导致串行队列中追加的任务和串行队列中原有的任务两者之间相互等待,阻塞了串行队列,最终造成了串行队列所在的线程(子线程)死锁问题。
四、GCD 线程间的通信
  • 在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
	// 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        // 异步追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程
        
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2 : %@",[NSThread currentThread]);      // 打印当前线程
        });
    });
  • 执行结果如下:
	2021-03-24 21:12:08.140921+0800 GCD[48987:7842436] 1 : <NSThread: 0x6000029e5340>{number = 5, name = (null)}
	2021-03-24 21:12:10.142362+0800 GCD[48987:7842289] 2 : <NSThread: 0x6000029a8000>{number = 1, name = main}
  • 可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。
五、GCD 其他方法
① GCD 栅栏方法:dispatch_barrier_async
  • 我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,这样就需要一个相当于“栅栏”一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务,这就需要用到dispatch_barrier_async 方法在两个操作组间形成栅栏。
  • dispatch_barrier_async 方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。具体如下图所示:

在这里插入图片描述

  • 使用如下:
	dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"barrier : %@",[NSThread currentThread]);// 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 4
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"4 : %@",[NSThread currentThread]);      // 打印当前线程
    });
  • 执行结果如下:
	2021-03-24 21:27:48.490812+0800 GCD[49065:7858826] 1 : <NSThread: 0x600003976000>{number = 6, name = (null)}
	2021-03-24 21:27:48.490856+0800 GCD[49065:7858825] 2 : <NSThread: 0x600003973600>{number = 7, name = (null)}
	2021-03-24 21:27:50.494787+0800 GCD[49065:7858825] barrier : <NSThread: 0x600003973600>{number = 7, name = (null)}
	2021-03-24 21:27:52.496547+0800 GCD[49065:7858825] 3 : <NSThread: 0x600003973600>{number = 7, name = (null)}
	2021-03-24 21:27:52.496590+0800 GCD[49065:7858826] 4 : <NSThread: 0x600003976000>{number = 6, name = (null)}
  • 在 dispatch_barrier_async 执行结果中可以看出:在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
② GCD 延时执行方法:dispatch_after
  • 我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务,这个时候,可以用 GCD 的dispatch_after 方法来实现。
  • 需要注意的是:dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0 秒后异步追加任务代码到主队列,并开始执行
        NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程
    });
③ GCD 一次性代码(只执行一次):dispatch_once
  • 在创建单例或者有整个程序运行过程中只执行一次的代码时,就需要用到 GCD 的 dispatch_once 方法。
  • 使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。
	static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });
④ GCD 快速迭代方法:dispatch_apply
  • 通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法 dispatch_apply。
  • dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
  • 如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。
  • 可以利用并发队列进行异步执行:比如说遍历 0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历,dispatch_apply 可以在多个线程中同时(异步)遍历多个数字。
  • 无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。
	dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSLog(@"apply--begin");
    dispatch_apply(6, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply--end");
  • 执行结果:
	2021-03-24 21:34:02.610819+0800 GCD[49119:7865760] apply--begin
	2021-03-24 21:34:02.611022+0800 GCD[49119:7865760] 0---<NSThread: 0x6000004d41c0>{number = 1, name = main}
	2021-03-24 21:34:02.611026+0800 GCD[49119:7865874] 1---<NSThread: 0x600000499800>{number = 3, name = (null)}
	2021-03-24 21:34:02.611056+0800 GCD[49119:7865872] 3---<NSThread: 0x60000048c3c0>{number = 5, name = (null)}
	2021-03-24 21:34:02.611056+0800 GCD[49119:7865871] 2---<NSThread: 0x60000048c840>{number = 6, name = (null)}
	2021-03-24 21:34:02.611099+0800 GCD[49119:7865875] 4---<NSThread: 0x60000048dc80>{number = 7, name = (null)}
	2021-03-24 21:34:02.611140+0800 GCD[49119:7865760] 5---<NSThread: 0x6000004d41c0>{number = 1, name = main}
	2021-03-24 21:34:02.611215+0800 GCD[49119:7865760] apply--end
  • 这是因为在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是 apply—end 一定在最后执行,是因为 dispatch_apply 方法会等待全部任务执行完毕。
⑤ GCD 队列组:dispatch_group
  • 有时候会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务,这时候我们可以用到 GCD 的队列组。
  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中,或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合来实现 dispatch_group_async。
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务,或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
  • dispatch_group_notify
    • 监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
    • 使用如下:
	NSLog(@"group--begin");
	
	dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3 : %@",[NSThread currentThread]);      // 打印当前线程

        NSLog(@"group--end");
    });
    • 从 dispatch_group_notify 相关代码运行输出结果可以看出:
      当所有任务都执行完成之后,才执行 dispatch_group_notify 相关 block 中的任务。
  • dispatch_group_wait
    • 暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
    • 使用如下:
	NSLog(@"group--begin");
    
	dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2 : %@",[NSThread currentThread]);      // 打印当前线程
    });
    
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"group--end");
    • 从 dispatch_group_wait 相关代码运行输出结果可以看出:
      当所有任务执行完成之后,才执行 dispatch_group_wait 之后的操作。
    • 注意:使用dispatch_group_wait 会阻塞当前线程。
  • dispatch_group_enter、dispatch_group_leave
    • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1;
    • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1;
    • 当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。
	NSLog(@"group--begin");
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程

        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2 : %@",[NSThread currentThread]);      // 打印当前线程
        
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3 : %@",[NSThread currentThread]);      // 打印当前线程
    
        NSLog(@"group--end");
    });
    • 从 dispatch_group_enter、dispatch_group_leave 相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务,这里的dispatch_group_enter、dispatch_group_leave 组合,其实也等同于dispatch_group_async。
⑥ GCD 信号量:dispatch_semaphore
  • GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号,类似于过高速路收费站的栏杆,可以通过时,打开栏杆,不可以通过时,关闭栏杆。
  • 在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,可通过。
  • Dispatch Semaphore 提供了三个方法:
    • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量;
    • dispatch_semaphore_signal:发送一个信号,让信号总量加 1;
    • dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
    • 注意:信号量的使用前提是,想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
  • Dispatch Semaphore 在实际开发中主要用于:
    • 保持线程同步,将异步执行任务转换为同步执行任务;
    • 保证线程安全,为线程加锁。
  • Dispatch Semaphore 线程同步
    • 在开发中,可能会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。
    • 比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法,通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
	 - (NSArray *)tasksForKeyPath:(NSString *)keyPath {
	    __block NSArray *tasks = nil;
	    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
	    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
	        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
	            tasks = dataTasks;
	        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
	            tasks = uploadTasks;
	        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
	            tasks = downloadTasks;
	        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
	            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
	        }
	
	        dispatch_semaphore_signal(semaphore);
	    }];
	
	    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
	
	    return tasks;
	}
    • 下面来利用 Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务。
		NSLog(@"currentThread--%@",[NSThread currentThread]);  // 打印当前线程
	    NSLog(@"semaphore--begin");
	    
	    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;
	    dispatch_async(queue, ^{
	        // 追加任务 1
	        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
	        NSLog(@"1 : %@",[NSThread currentThread]);      // 打印当前线程
	        
	        number = 100;
	        
	        dispatch_semaphore_signal(semaphore);
	    });
	    
	    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
	    NSLog(@"semaphore--end,number = %zd",number);
    • 从 Dispatch Semaphore 实现线程同步的代码可以看到:semaphore–end 是在执行完 number = 100; 之后才打印的,而且输出结果 number 为 100。这是因为异步执行不会做任何等待,可以继续执行任务。执行顺如下:
      • semaphore 初始创建时计数为 0。
      • 异步执行 将 任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
      • 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行。
      • 最后打印 semaphore—end,number = 100。
    • 这样就实现了线程同步,将异步执行任务转换为同步执行任务。
  • Dispatch Semaphore 线程安全和线程同步(为线程加锁)
    • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
    • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
⑦ dispatch_source
  • dispatch_source 是基础数据类型,用于协调特定底层系统事件的处理。
  • dispatch_source 替代了异步回调函数,来处理系统相关的事件,当配置一个dispatch 时,你需要指定监测的事件、dispatch queue、以及处理事件的代码(block或函数)。当事件发生时,dispatch source会提交你的block或函数到指定的queue去执行;
  • 使用 dispatch_source 而不使用 dispatch_async 的主要原因就是利用联结的优势;
  • 联结就是:在任一线程上调用它的的一个函数 dispatch_ source_ merge_ data 后,会执行 dispatch_source 事先定义好的句柄(可以把句柄简单理解为一个block),这个过程叫 Custom event 用户事件,是 dispatch source 支持处理的一种事件,即为调用 dispatch_source_merge_data 函数来向自己发出的信号。
  • 句柄是一种指向指针的指针,它指向的就是一个类或者结构,和系统有很密切的关系,HINSTANCE (实例句柄)、HBITMAP (位图句柄) 、HDC (设备表述句柄)、 HICON(图标句柄)等,这当中还有一个通用的句柄,就是HANDLE;
    • 实例句柄 HINSTANCE
    • 位图句柄 HBITMAP
    • 设备表句柄 HDC
    • 图标句柄 HICON
  • dispatch_source 的CPU负荷非常小,尽量不占用资源 。
  • dispatch_source 的创建:
	// type	  dispatch源可处理的事件
	// handle 可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID
	// mask	  可以理解为描述,提供更详细的描述,让它知道具体要监听什么
	// queue  自定义源需要的一个队列,用来处理所有的响应句柄
	dispatch_source_t source = dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue)
  • dispatch_source 的类型:
种类说明
DISPATCH_SOURCE_TYPE_DATA_ADD自定义的事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR自定义的事件,变量OR
DISPATCH_SOURCE_TYPE_MACH_SENDMACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECVMACH端口接收
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE内存压力 (注:iOS8后可用)
DISPATCH_SOURCE_TYPE_PROC进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READIO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER定时器
DISPATCH_SOURCE_TYPE_VNODE文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITEIO操作,如对文件的操作、socket操作的写响应
  • dispatch_source 常用函数:
	// 挂起队列
	dispatch_suspend(queue) 
	// 分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复
	dispatch_resume(source) 
	// 向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。
	dispatch_source_merge_data 
	// 设置响应分派源事件的block,在分派源指定的队列上运行
	dispatch_source_set_event_handler 
	// 得到分派源的数据
	dispatch_source_get_data 
	// 得到dispatch源创建,即调用dispatch_source_create的第二个参数
	uintptr_t dispatch_source_get_handle(dispatch_source_t source); 
	// 得到dispatch源创建,即调用dispatch_source_create的第三个参数
	unsigned long dispatch_source_get_mask(dispatch_source_t source); 
	// 取消dispatch源的事件处理--即不再调用block。如果调用dispatch_suspend只是暂停dispatch源。
	void dispatch_source_cancel(dispatch_source_t source); 
	// 检测是否dispatch源被取消,如果返回非0值则表明dispatch源已经被取消
	long dispatch_source_testcancel(dispatch_source_t source); 
	// dispatch源取消时调用的block,一般用于关闭文件或socket等,释放相关资源
	void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t cancel_handler); 
	// 可用于设置dispatch源启动时调用block,调用完成后即释放这个block。也可在dispatch源运行当中随时调用这个函数。
	void dispatch_source_set_registration_handler(dispatch_source_t source, dispatch_block_t registration_handler); 
  • dispatch_source 使用场景:由于 dispatch_source 不依赖于 Runloop,而是直接和底层内核交互,准确性更高,所以经常用于验证码倒计时。
	- (void)use {
	    // 倒计时时间
	    __block int timeout = 3;
	    
	    // 创建队列
	    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
	    
	    // 创建timer
	    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
	    
	    // 设置1s触发一次,0s的误差
	    /*
	     - source 分派源
	     - start 数控制计时器第一次触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。
	     - interval 间隔时间
	     - leeway 计时器触发的精准程度
	     */
	    dispatch_source_set_timer(timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0);
	    
	     // 触发的事件
	    dispatch_source_set_event_handler(timer, ^{
	        // 倒计时结束,关闭
	        if (timeout <= 0) {
	            // 取消dispatch源
	            dispatch_source_cancel(timer);
	        }else{
	            timeout--;
	            
	            dispatch_async(dispatch_get_main_queue(), ^{
	                // 更新主界面的操作
	                NSLog(@"倒计时 - %d", timeout);
	            });
	        }
	    });
	    
	    // 开始执行dispatch源
	    dispatch_resume(timer);
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ栺尖篴夢ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值