GCD(Grand Central Dispatch)是苹果公司专门为多核并发运算提出的解决方案,是纯C语言的,提供了很多非常强大的函数。本文主要介绍串行队列和并行队列以及同步和异步往队列里添加任务的区别。
1.GCD的优势
(1)会自动利用更多的CPU内核(从iPhone4s开始是双核,iPhone7系列是4核)。
(2)会自动管理线程的生命周期(创建、销毁)等,不需要写一句关于线程的代码。
(3)将任务添加到队列中,GCD会自动从队列中将任务取出,放到对应的线程中执行。
2.队列(queue)
GCD相较于NSThread最大的改变就是:NSThread是直接把任务放在线程里,而GCD是把任务放在队列里。队列就是盛放任务的容器。GCD会根据CPU的使用情况自己创建线程,执行队列里的任务。队列里任务的调度遵行FIFO原则,就是哪个任务先放进队列中,就先取出来执行。
队列分为串行队列,和并行(并发)队列。
串行队列 : 就是队列里的任务前一个执行完,后一个才开始执行。串行队列里异步添加的任务都在一个线程里执行。
创建串行队列有两种方法:
(1)自定义串行队列
// 创建串行队列
// doujiangyoutiao 队列的名字
// DISPATCH_QUEUE_SERIAL 表示创建的是串行队列
dispatch_queue_t queue = dispatch_queue_create("doujiangyoutiao", DISPATCH_QUEUE_SERIAL);
// 添加任务A
dispatch_async(queue, ^{
NSLog(@"执行任务A");
});
// 添加任务B
dispatch_async(queue, ^{
NSLog(@"执行任务B");
});
// 添加任务C
dispatch_async(queue, ^{
NSLog(@"执行任务C");
});
日志
执行任务A
执行任务B
执行任务C
(2)主队列
// 得到主队列
dispatch_queue_t queue =dispatch_get_main_queue();
主队列是串行队列,主队列中的任务默认都在主线程中执行。
并行队列:就是后面添加的任务不必等后面的任务执行完再开始执行,有可能一起开始执行。所以并行任务里第一个添加的任务并不一定第一个执行完。
创建并行队列也有两种方法:
(1)和创建串行队列、添加任务的方式一样。
// 创建并行队列
// 豆丶浆油条 队列的名字
// DISPATCH_QUEUE_CONCURRENT 表示创建的是并行队列
dispatch_queue_t queue = dispatch_queue_create("豆丶浆油条",DISPATCH_QUEUE_CONCURRENT);
(2)获取系统的全局并发队列。
- (void)viewDidLoad {
[super viewDidLoad];
// 获取系统的全局并发队列
// DISPATCH_QUEUE_PRIORITY_DEFAULT表示队列的优先级一共三个值:DISPATCH_QUEUE_PRIORITY_HIGH、DISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW、DISPATCH_QUEUE_PRIORITY_BACKGROUND
// 第二个参数是预留参数,不起作用,传0就行
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 添加任务A
dispatch_async(queue, ^{
NSLog(@"执行任务A");
});
// 添加任务B
dispatch_async(queue, ^{
NSLog(@"执行任务B");
});
// 添加任务C
dispatch_async(queue, ^{
NSLog(@"执行任务C");
});
}
每次打印的日志应该都不一样,有兴趣可以多试几次。并行队列里的任务执行完的顺序是不固定的。
执行任务B
执行任务C
执行任务A
注意:
- 并发队列里的任务一定会一起执行嘛?
答:不一定。如果并发队列里有100个任务,一起执行就得创建100条线程,那不得累死CPU啊!任务少了有可能会一起执行,任务多了会先一起执行一部分。 - 线程并发???
答:错。串行并发是形容队列的!!!线程没有,线程里的任务都是一个一个的往下执行,不可能多个任务一起执行。
3.同步和异步
同步和异步是指向队列里添加任务的方式,上文往线程里都是异步添加任务的。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("豆丶浆油条", DISPATCH_QUEUE_CONCURRENT);
// 异步向队列里添加任务
dispatch_async(queue, ^{
NSLog(@"任务1------%@",[NSThread currentThread]);
});
// 同步向队列里添加任务
dispatch_sync(queue, ^{
sleep(1);
NSLog(@"任务2------%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务3------%@",[NSThread currentThread]);
});
}
日志
任务1------{number = 3, name = (null)}
任务2------{number = 1, name = main}
任务3------{number = 3, name = (null)}
由于同步任务都是在当前线程执行的,并且它执行完以后,后面的任务才能执行,所以会堵塞当前线程。例子中由于任务2是在主线程里添加的,尽管执行了挂起1秒,但是后面的任务3也要等任务2执行完以后再执行,这样就堵塞了主线程。所以尽量不要在主线程中添加同步任务,会影响用户交互。
异步添加任务不会堵塞当前线程,也不会影响后面任务的执行。所以我们尽量使用异步向队列中添加任务。
4. 用GCD实现多线程并发
用GCD实现多线程并发必须满足两个条件:并发队列;异步向队列里添加任务。
- (void)viewDidLoad {
[super viewDidLoad];
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 可以自己创建并发队列
// dispatch_queue_t queue = dispatch_queue_create("豆浆油条", DISPATCH_QUEUE_CONCURRENT);
// 异步添加任务1
dispatch_async(queue, ^{
NSLog(@"任务1开始执行");
sleep(1);
NSLog(@"任务1执行完了");
});
// 异步添加任务2
dispatch_async(queue, ^{
NSLog(@"任务2开始执行");
sleep(2);
NSLog(@"任务2执行完了");
});
// 异步添加任务3
dispatch_async(queue, ^{
NSLog(@"任务3开始执行");
sleep(3);
NSLog(@"任务3执行完了");
});
}
日志
2019-04-20 23:27:54.081762+0800 任务1开始执行
2019-04-20 23:27:54.082177+0800 任务2开始执行
2019-04-20 23:27:54.082255+0800 任务3开始执行
2019-04-20 23:27:55.087043+0800 任务1执行完了
2019-04-20 23:27:56.087664+0800 任务2执行完了
2019-04-20 23:27:57.083123+0800 任务3执行完了
我们往全局并发队列里异步添加了三个任务,三个任务几乎同时开始执行。这里需要注意的是并不是向队列里添加了多少个任务,这些任务就会全部一起执行,这得看CPU的运行状况,系统会选择几个任务一起执行,剩下的任务等先执行的任务执行完了再执行。
5. 向主队列中添加任务
主队列的任务默认都是在主线程中执行的,所以如果我们有需要更新UI或者处理用户交互的任务就必须放在主队列中。这里需要注意的是只能异步向主队列中添加任务,否则会发生死锁。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"task 1");
});
NSLog(@"task 2");
}
日志
task1
任务2并没有执行,此时程序卡主了。这就叫做死锁。这是因为任务2是直接添加在主线程里,任务1是添加在主队列里的,主线程要执行完自己的任务,才会执行主队列里的任务,所以任务1要等任务2执行完才能执行。而任务1又是同步添加的,它不执行完,下面的任务就不能执行。这就造成了任务1和任务2相互等待,谁都不执行。
任务1对任务2说:你快执行啊?你执行完我才能执行!
任务2对任务1说:不行啊,你执行完我们才能执行。这是同步那个二货规定的。
任务1和任务2就开始了漫长的等待……
6.dispatch_group_t的使用
有这样一个需求,一张图片需要两张图片合成,而这两张图片是分别下载的,所以我们要等两张图片都下载完了,才能合成。由于我们不知道哪张图片先下载完,只能是一张下载完了再下载另一张,最后合成。这样浪费时间,因为我们是可以同时两张图片的。
GCD提供了队列组,只有队列里的任务全部执行完,才会执行回调。这样我们就可以同时下载两张图片了,而不用担心合成的时候有一张没下载完。
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView1;
@property (weak, nonatomic) IBOutlet UIImageView *imageView2;
@property (weak, nonatomic) IBOutlet UIImageView *imageView3;
@end
@implementation ViewController
// 点击屏幕开始下载图片
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 获得全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建队列组
dispatch_group_t group = dispatch_group_create();
__block UIImage *image1 = nil;
// 开启一个任务
dispatch_group_async(group, queue, ^{
NSLog(@"%@开始下载第一张图片",[NSThread currentThread]);
NSString *strURL1 = @"http://h.hiphotos.baidu.com/zhidao/pic/item/6d81800a19d8bc3ed69473cb848ba61ea8d34516.jpg";
image1 = [self downloadImageWithURL:strURL1];
});
// 开启一个任务
__block UIImage *image2 = nil;
dispatch_group_async(group, queue, ^{
NSLog(@"%@开始下载第二张图片",[NSThread currentThread]);
NSString *strURL2 = @"http://h.hiphotos.baidu.com/zhidao/pic/item/0eb30f2442a7d9334f268ca9a84bd11372f00159.jpg";
image2 = [self downloadImageWithURL:strURL2];
});
// 同时执行下载图片1\下载图片2的操作
// 等group里的任务全部执行完毕,执行 dispatch_group_notify回调
// 回到主线程显示图片
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"%@显示图片",[NSThread currentThread]);
self.imageView1.image = image1;
self.imageView2.image = image2;
// 合并两张图片图片
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 50), NO, 0.0);
[image1 drawInRect:CGRectMake(0, 0, 50, 50)];
[image2 drawInRect:CGRectMake(50, 0, 50, 50)];
self.imageView3.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
}
- (UIImage *)downloadImageWithURL : (NSString *)strURL {
NSURL *url = [NSURL URLWithString:strURL];
return [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
}
@end
日志
2016-11-05 23:03:49.300 {number = 3, name = (null)}开始下载第一张图片
2016-11-05 23:03:49.301 {number = 4, name = (null)}开始下载第二张图片
2016-11-05 23:03:49.453 {number = 1, name = main}显示图片
效果:
7.延迟执行
延迟执行就是让程序过一会再执行某个任务。有两种方法:
- (void)viewDidLoad {
[super viewDidLoad];
// 第一种
[self performSelector:@selector(run) withObject:nil afterDelay:2];
// 第二种 可以安排执行的队列 3秒后执行
// 主队列
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"当前线程%@",[NSThread currentThread]);
NSLog(@"GCD主队列过一会执行我");
});
// 全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
NSLog(@"当前线程3%@",[NSThread currentThread]);
NSLog(@"GCD全局并发队列过一会执行我");
});
}
- (void)run {
NSLog(@"当前线程1%@",[NSThread currentThread]);
NSLog(@"过一会执行我");
}
日志
当前线程1{number = 1, name = main}
过一会执行我
当前线程2{number = 1, name = main}
当前线程3{number = 3, name = (null)}
GCD主队列过一会执行我
GCD全局并发队列过一会执行我
延迟执行我们常用的可能是第一种方法。从那个线程中调用“ performSelector”,run方法就在哪个线程中执行可,一般是主线程。第二种方法是可以自定义方法执行的队列,可以是主队列,也可以是全局队列。本人比较喜欢用block,所以喜欢第二种,因为都写在一起,增加了代码的可读性。
8.一次性代码
一次性代码主要是在单例中应用。
#import "Person.h"
@implementation Person
static Person *person;
// 1
- (instancetype)shareInstance1 {
static dispatch_once_t onceToken;
// block里的代码只在一个进程周期中执行一次
dispatch_once(&onceToken, ^{
person = [[Person alloc] init];
});
return person;
}
// 2.
- (instancetype)shareInstace2 {
if (!person) {
person = [[Person alloc] init];
}
return person;
}
@end
dispatch_once里的代码在整个程序运行过程中就执行一次!!!所以你有这方面的需求,也可以用这个。
关于GCD还有很多强大的函数,并且是开源的,欢迎大家补充说明,不吝赐教。