【iOS多线程(一)】GCD详细总结-同步异步/串行并发

18 篇文章 0 订阅
5 篇文章 0 订阅

本章如何复习?

  1. 首先记住两个核心概念,任务和队列的概念,各自影响的是什么
  2. 然后直接去复习我对它们的总结,下面有
  3. 一定要理解同步函数和异步函数到底是什么意思: 同步会阻塞当前函数的返回异步函数会立即返回,区执行下面的代码
  4. 记住死锁产生条件:使用sync函数当前的串行的队列 中添加任务,就会产生死锁。其他情况都不会产生死锁。
  5. 看那张表,了解如何判断,是否开启新线程,串行还是并发执行任务

1. GCD简介

GCD(Grand Central Dispatch)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上,执行的并发任务。

GCD的好处具体如下:

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

2. GCD任务和队列

GCD有两个核心概念:任务和队列

任务

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的

  • 同步执行(sync)
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力
  • 异步执行(async)
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行后面的任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});

队列 (dispatch是派遣的意思)

队列(Dispatch Queue) : 这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进线出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,从队列中释放一个任务。

  • 串行队列(Serial Dispatch Queue)
    • 每次只有一个任务被执行。让任务一个接着一个地执行。(开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并发队列(Concurrent Dispatch Queue)
    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

并发队列的并发功能只有在异步函数(dispatch_async)下才有效

队列的创建方法和获取方法

可以使用dispatch_queue_create来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,用于DEBUG,可以为空,Dispatch Queue的名称推荐使用应用程序ID这中逆序全称域名;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。


// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);

对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue)

  • 所有放在主队列中的任务,都会放到主线程中执行
  • 可使用dispatch_get_main_queue()获得主队列

// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

对于并发队列,GCD默认提供了全局并发队列(Global Dispatch Queue)
可以使用dispatch_get_global_queue来获取。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没有用,传0即可。


// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

在这里插入图片描述

结论:

  1. 使用sync函数往串行队列中添加任务,会卡住当前的串行队列,产生死锁
  2. 并发功能只有在异步函数才会生效

3.我的总结:同步和异步函数,并行和并发队列

同步异步函数

同步函数(sync)和异步函数(async),只能决定能否开启新的线程执行任务,不能决定函数是串行执行还是并行执行

同步:

  • 在当前线程中执行,不具备开启新线程的能力
  • 后面的内容需要等同步函数返回了,才能执行。所以就在当前线程执行就行,不需要开启新的线程,因此也不具备开新线程的能力
    异步:
  • 在新的线程中执行,具备开启新线程的能力(注意,是具备开新线程的能力,不一定会开启新线程)
  • 后面的内容不需要等异步函数返回才执行,后面的内容可以直接执行。所以需要开启新线程,因此具备开新线程的能力

同步函数和异步函数:同步会阻塞当前函数的返回异步函数会立即返回,区执行下面的代码

最重要的一句理解:!!!

同步函数后面的内容需要等同步函数返回(执行完了),才能执行。异步函数后面的内容不需要等异步函数返回才执行,可以直接执行(就好像可以跳过这个异步函数一样),而这个异步函数里面的内容可能就需要开启一个新线程来执行。

串行并发队列

队列:并发和串行。主要影响任务的执行方式,能不能开启新线程取决于上面的两个函数
并发:多个任务并发执行
串行:一个任务执行完毕后,再执行下一个任务

是否开启新线程,串行还是并发执行任务,如何分析?

  1. 同步函数(sync),代表没有开新线程,一定是串行执行

  2. 异步函数(async),代表如果需要,可以开新线程

    • 如果传的是并发队列,就并发执行任务(可能开启多条线程,由操作系统决定)
    • 如果传的是手动创建的串行队列,就在子线程串行执行(开启一条新线程)
    • 如果传的是主队列,就在主线程中串行执行(没有开启新线程)
    • 什么情况下不会开启新线程呢?传入的是主队列

在这里插入图片描述

死锁条件总结:

使用sync函数当前的串行的队列 中添加任务,就会产生死锁。其他情况都不会产生死锁。
三个关键点:

  1. 同步函数
  2. 当前队列 (在两个不同的队列就不会)
  3. 当前队列是串行队列 (并发队列就不会)

在主线程中用同步函数往(当前)主队列中添加任务,是常见产生死锁的情况之一
(注意要是在主线程中添加,如果你新开了一个线程,在新线程中,用同步函数往主队列添加任务是不会产生死锁的)

4. GCD基本使用

同步串行队列


dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
	// 追加任务1
	for (int i = 0; i < 2; ++i) {
		NSLog(@"1---%@",[NSThread currentThread]);
	}
});

dispatch_sync(queue, ^{
	// 追加任务2
	for (int i = 0; i < 2; ++i) {
		NSLog(@"2---%@",[NSThread currentThread]);
	}
});

在这里插入图片描述

结论:同步串行队列即没有开启新的线程,也没有异步执行

同步并行队列


dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
	// 追加任务1
	for (int i = 0; i < 2; ++i) {
		NSLog(@"1---%@",[NSThread currentThread]);
	}
});

dispatch_sync(queue, ^{
	// 追加任务2
	for (int i = 0; i < 2; ++i) {
		NSLog(@"2---%@",[NSThread currentThread]);
	}
});

在这里插入图片描述

根据两种打印我们发现:同步函数既不会开启新的线程,也不会执行并发任务

异步串行队列


NSLog(@"主线程:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
	for (int i = 0; i < 2; ++i) {
		NSLog(@"1====%@",[NSThread currentThread]);      // 打印当前线程
	}
});

dispatch_async(queue, ^{
	for (int i = 0; i < 2; ++i) {
		NSLog(@"2====%@",[NSThread currentThread]);      // 打印当前线程
	}

});

在这里插入图片描述

结果:有开启新的线程,串行执行任务

异步并行队列


NSLog(@"主线程:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
	for (int i = 0; i < 2; ++i) {
		[NSThread sleepForTimeInterval:2];
		NSLog(@"1====%@",[NSThread currentThread]);      // 打印当前线程
	}

});
dispatch_async(queue, ^{
	for (int i = 0; i < 2; ++i) {
		[NSThread sleepForTimeInterval:2];
		NSLog(@"2====%@",[NSThread currentThread]);      // 打印当前线程
	}

});

在这里插入图片描述

结果:有开启新的线程,并发执行任务。想要出现明显的并发执行效果,可以sleep一下

sync函数造成的线程死锁

首先你要理解同步和异步执行的概念,同步和异步目的不是为了是否创建一个新的线程.
同步会阻塞当前函数的返回异步函数会立即返回,区执行下面的代码
队列是一种数据结构,队列有FIFO,LIFO等,控制任务的执行顺序,至于是否开辟一个新的线程,因为同步函数会等待函数的返回,所以在当前线程执行就行了,没必要浪费资源再开辟新的线程,如果是异步函数,当前线程需要立即函数返回,然后往下执行,所以函数里面的任务必须要开辟一个新的线程去执行这个任务。

队列上是放任务的,而线程是去执行队列上的任务的

问题一:以下代码是在主线程执行的,会不会产生死锁?会!


NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
	NSLog(@"执行任务2");
});

NSLog(@"执行任务3");

在这里插入图片描述

dispatch_sync立马在当前线程同步执行任务
分析:

    1. 主线程中任务执行: 任务1、sync、任务3
    1. 主队列: viewDidLoad、任务2

主队列中有首先有ViewDidLoad,当执行到第31行时,往主队列中同步添加了任务2。此时正确的执行顺序应该是,把主线程中的ViewDidLoad方法执行完,才能执行任务2。但是从这个方法出发,必须得先执行完任务2,才可能把ViewDidLoad执行完。所以造成死锁。与任务3无关。

问题2: 以下代码是,在一个新线程中,往主线程同步添加任务。会不会产生死锁?不会!


- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        NSLog(@"任务1");

        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"任务2");
        });

        NSLog(@"任务3");
    });
    
    NSLog(@"任务4");
}

分析:

    1. 主线程中任务执行:viewDidLoad,(async不在主队列执行),任务2
    1. 新开了一个新线程 : async(任务1,sync,任务2)

这里面的sync的意思也是,viewDidLoad执行完,才能执行任务2,而主线程里面ViewDidLoad确实执行完了,因为async根本不在主线程。所以执行完viewDidLoad以后,再执行加进主队列的任务2,没有问题。

5. 进程间通信


- (void)communication {
    //获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        //异步追加任务
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1----%@", [NSThread currentThread]);
        }
        
        //回到主线程
        dispatch_async(mainQueue, ^{
            //追加在主线程执行的任务
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2----%@", [NSThread currentThread]);
        });
    });
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self communication];
}

在这里插入图片描述

这就是一个异步,网络请求,加载图片,然后执行结束后返回主队列(给主队列添加任务,比如把数据传给主线程)。而主线程不会等待这个网络请求方法结束,会直接继续执行后续的代码。

6. GCD的其他方法

这一块知识我会单读写一篇博客详细讲

6.1 GCD栅栏方法:dispatch_barrier_async

在这里插入图片描述

就是我们在异步执行一些操作的时候,我们使用dispatch_barrier_async函数把异步操作暂时性的做成同步操作,就行一个栅栏一样分开。


- (void)viewDidLoad {
	[super viewDidLoad];

	self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

	for (int i = 0; i < 10; i++) {
		[self read];
		[self read];
		[self read];
		[self write];
	}
}

- (void)read {
	dispatch_async(self.queue, ^{
		sleep(1);
		NSLog(@"read");
	});
}

- (void)write
{
	dispatch_barrier_async(self.queue, ^{
		sleep(1);
		NSLog(@"write");
	});
}

在这里插入图片描述

我们观察时间可以看到在执行dispatch_barrier_async写操作的时候是同步执行的,不会出现异步情况

6.2、GCD 延时执行方法:dispatch_after

我们经常会遇到这样的需求:在指定时间(例如3秒)之后执行某个任务。可以用 GCD 的dispatch_after函数来实现。
需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

6.3、GCD 一次性代码(只执行一次):dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数


static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});

6.4 dispatch_group

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组

  • 调用队列组的 dispatch_group_async , 这个方法的作用是先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合 来实现dispatch_group_async
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1
  • 当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。

/**
* 队列组 dispatch_group_notify
*/
- (void)groupNotify {
	NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
	NSLog(@"group---begin");

	dispatch_group_t group =  dispatch_group_create();

	dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
		// 追加任务1
		for (int i = 0; i < 2; ++i) {
			[NSThread sleepForTimeInterval:2];              // 模拟耗时操作
			NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
		}
	});

	dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
		// 追加任务2
		for (int i = 0; i < 2; ++i) {
			[NSThread sleepForTimeInterval:2];              // 模拟耗时操作
			NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
		}
	});

	dispatch_group_notify(group, dispatch_get_main_queue(), ^{
		// 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
		for (int i = 0; i < 2; ++i) {
			[NSThread sleepForTimeInterval:2];              // 模拟耗时操作
			NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
		}
		NSLog(@"group---end");
	});
}

6.5 GCD信号量: dispatch_semaphore

Dispatch Semaphore 提供了三个函数。

  • dispatch_semaphore_create:创建一个信号量,具有整形的数值,即为信号的总量。
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值