1.关于GCD
Grand Central Dispatch是异步执行任务的技术之一。我们先看一个简单的示例:
- (void)doSomethingInBackground {
[self performSelectorInBackground:@selector(startWork) withObject:nil];
} - (void)startWork {
//长时间处理操作
NSLog(@"进行长时间处理"); [self performSelectorOnMainThread:@selector(endWork) withObject:nil waitUntilDone:YES];
} - (void)endWork {
NSLog(@"完成工作并刷新页面");
}
这个示例实现的功能很简单:在后台线程中执行长时间的处理,处理结束后,在主线程中使用处理结果。上面实现方式是使用NSObject类的performSelectorInBackground和performSelectorOnMainThread来实现的。下面我们用GCD的方式来实现同样功能,做个对比:
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"进行长时间处理");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"完成工作并刷新页面");
});
});
通过上面的比较,我们可以知道,GCD更加的简洁,同样的功能,代码量减少了差不多一半。
2.多线程编程
什么是线程?线程(英语:thread)是操作系统能够进行运算调度的最小单位(来自维基百科)。iOS应用程序在启动后,首先会将包含在应用程序中的CPU指令配置到内存中,CPU从应用程序指定的地址开始,一个一个去执行CPU指令,一个CPU一次只能执行一个指令。由于现在的物理CPU芯片实际上有64个(64核)CPU,如果1个CPU核虚拟为两个CPU核工作,那么使用多个CPU核同时工作就是很正常的事情了。这种存在多个CPU核工作的即为“多线程”。
在实际开发中,多线程编程是一种容易发生问题的编程技术。我们常见的有这么几种:
- 数据竞争:多个线程更新相同的资源会导致数据的不一致;
- 死锁:多个线程相互持续等待;
- 内存消耗高:每个线程都是有内存开销的,太多线程会大量的消耗内存。
多线程存在上面的这些问题,那么我们是不是应该弃用呢?答案是否定的,因为使用多线程编程,可以让我们在执行长时间的处理时,保证用户界面的即时响应。
3.GCD的API
3.1Dispatch Queue
分发队列主要有两类:
- Serial Dispatch Queue:等待现在执行任务处理结束;
- Concurrent Dispatch Queue:不等待现在执行处理结束;
听概念是不是感觉很抽象?没关系,下面我们来看看代码。
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{
NSLog(@"blk");
}); dispatch_async(queue, ^{
NSLog(@"blk");
}); dispatch_async(queue, ^{
NSLog(@"blk");
}); dispatch_async(queue, ^{
NSLog(@"blk");
}); dispatch_async(queue, ^{
NSLog(@"blk");
});
可以看到,上面我们使用的是Serial Dispatch Queue,运行效果会怎么样呢?
可以看到:使用Serial Dispatch Queue,因为要等待正在执行的任务结束,并且同事执行的处理数只能有一个,所以会依次打印blk1、blk2、blk3、blk4、blk5。
现在将上面示例的DISPATCH_QUEUE_SERIAL改为DISPATCH_QUEUE_CONCURRENT,结果会怎么样呢?
结论是:使用Concurrent Dispatch Queue时,因为不用等待正在执行的任务结束,可以并行执行多个任务,也就是使用多个线程同时执行,但并行执行任务的数量取决于系统当前的状态。上面并行执行的线程情况截图如下:
3.2dispatch_queue_create
上面介绍了dispatch queue,那么怎么创建分发队列呢?我们可以使用GCD的API函数。
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_create函数的第一个参数指定队列的名称,这个名称会在XCode调试中显示,也会显示在应用程序崩溃时所生成的CrashLog中。因此我们命名最好时取一些有意义的,以方便我们来定位问题和代码。例如下面的代码:
dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); dispatch_async(queue1, ^{
NSLog(@"blk1");
}); dispatch_async(queue2, ^{
NSArray *arr = @[@, @, @];
NSLog(@"%@", arr[]);
}); dispatch_async(queue3, ^{
NSLog(@"blk3");
}); dispatch_async(queue4, ^{
NSLog(@"blk4");
}); dispatch_async(queue5, ^{
NSLog(@"blk5");
});
从代码可以看到,queue2会崩溃,实际运行后的CrashLog如下:
dispatch_queue_create函数的第二个参数指定创建队列的类型。如果想创建Serial Dispatch Queue,那么指定类型为DISPATCH_QUEUE_SERIAL或NULL;如果想创建Concurrent Dispatch Queue,那么指定类型为DISPATCH_QUEUE_CONCURRENT。
【注意】:dispatch_queue_create函数可以创建任意个Dispatch Queue,当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue会并行执行,系统对于每一个Serial Dispatch Queue会生成并使用一个线程。如果生成大量的Serial Dispatch Queue,那么就会创建很多线程。过多使用线程,会消耗大量内存,降低系统的响应性能。
3.3Main Dispatch Queue/Global Dispatch Queue
上面说了用dispatch_queue_create函数生成Dispatch Queue,现在聊一下系统标准提供的几个Dispatch Queue。
名称 | Dispatch Queue类型 | 说明 |
Main Dispatch Queue | Serial Dispatch Queue | 主线程执行 |
Global Dispatch Queue(High Priority) | Concurrent Dispatch Queue | 执行优先级:高 |
Global Dispatch Queue(Default Priority) | Concurrent Dispatch Queue | 执行优先级:默认 |
Global Dispatch Queue(Low Priority) | Concurrent Dispatch Queue | 执行优先级:低 |
Global Dispatch Queue(Background Priority) | Concurrent Dispatch Queue | 执行优先级:后台 |
怎么创建?
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, );
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, );
dispatch_queue_t lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, );
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, );
3.4dispatch_after
从这个函数名称,我们也能猜到它的大概作用:在xx时间后,再做某件事情。
NSLog(@"Before:%@", [NSDate date]);
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"After:%@", [NSDate date]);
});
【注意】:dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间后追加处理(也就是Block)到Dispatch Queue。
3.5Dispatch_Group
假设现在有个新的需求:需要在 Dispatch Queue中的多个任务完成之后,再做某件事情。当然大家会马上反应过来,用Serial Dispatch Queue即可。但如果应用场景是Concurrent Dispatch Queue或者多个Dispatch Queue呢?
这个时候就需要Dispatch Group了。
dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue1, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue2, ^{
NSLog(@"blk2");
});
dispatch_group_async(group, queue3, ^{
NSLog(@"blk3");
});
dispatch_group_async(group, queue4, ^{
NSLog(@"blk4");
});
dispatch_group_async(group, queue5, ^{
NSLog(@"blk5");
}); dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
多次运行,我们发现打印done总是在最后:
3.6dispatch_barrier_async
闭上眼睛,来想象这么一个场景:假设我们有一个本地数据库,会频繁的去读取数据和写入数据;写入是一个特殊的操作,如果在写入的时候有其他的读取操作,那么很有可能会读到脏数据,甚至也可能会造成程序的崩溃;如果只是单纯的读取操作,那么多个并行就不会发生问题。
针对这种场景,我们该怎么办呢?
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{
NSLog(@"blk1");
});
dispatch_async(queue, ^{
NSLog(@"blk2");
});
dispatch_async(queue, ^{
NSLog(@"blk3");
});
dispatch_barrier_async(queue, ^{
NSLog(@"blk0");
});
dispatch_async(queue, ^{
NSLog(@"blk4");
});
dispatch_async(queue, ^{
NSLog(@"blk5");
});
dispatch_async(queue, ^{
NSLog(@"blk6");
});
多次运行看结果:
通过多次运行,我们发现,不管怎么变化,blk0的打印永远在blk1、blk2、blk3的后面,blk4、blk5、blk6的前面。
3.7dispatch_sync
前面一直在说的都是“async”,意思就是非同步的将Block追加到指定的Dispatch Queue,dispatch_async函数不做任何等待;
对应的就有“sync”,意思就是同步的将Block追加到指定的Dispatch Queue。在追加Block结束之前,dispatch_sync函数会一直等待。
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"blk1");
});
dispatch_sync(queue, ^{
NSLog(@"blk2");
});
dispatch_sync(queue, ^{
NSLog(@"blk3");
});
dispatch_sync(queue, ^{
NSLog(@"blk4");
});
dispatch_sync(queue, ^{
NSLog(@"blk5");
}); NSLog(@"主线程");
多次运行看看结果,发现结果总是固定的,另外查看线程,执行都是在主线程。
如果使用dispatch_async呢?结果怎样?多次运行,可以发现顺序是随机的。
学到这里,我们已经接触到了这么4个概念:同步、异步、串行、并行。下面总结一下:
- 队列分为串行和并行,任务的执行分为同步和异步,异步是多线程的代名词,异步在实际应用中会开启新的线程,执行耗时操作;
- 队列只是负责任务的调度,而不负责任务的执行,任务是在线程中执行。
- 同步意味着在当前线程中执行,异步意味着会开启新的线程。
下面我们继续来细细品味一下这4个概念:串行同步、串行异步、并行同步、并行异步。
3.7.1串行同步
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"blk1 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk2 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk3 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk4 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk5 thread:%@", [NSThread currentThread]);
}); NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。
3.7.2串行异步
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"blk1 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk2 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk3 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk4 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk5 thread:%@", [NSThread currentThread]);
}); NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务放进别的线程,不阻塞当前线程,等任务执行完成,开始下一个任务。
3.7.3并行同步
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"blk1 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk2 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk3 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk4 thread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"blk5 thread:%@", [NSThread currentThread]);
}); NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。
3.7.4并行异步
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"blk1 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk2 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk3 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk4 thread:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"blk5 thread:%@", [NSThread currentThread]);
}); NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务放进别的线程,不阻塞当前线程,不等任务执行完成,开始下一个任务(会开启多个线程)。
3.7.5总结
串行(Serial Dispatch Queue) | 并行(Concurrent Dispatch Queue) | 主线程(Main Queue) | |
同步(sync) | 没有开启新线程 串行执行任务 | 没有开启新线程 串行执行任务 | 没有开启新线程 串行执行任务 |
异步(async) | 开启一个新线程 串行执行任务 | 开启多个新线程 并行执行任务 | 没有开启新线程 串行执行任务 |
3.7.6死锁
死锁是同步(sync)线程操作中比较常见的一个问题,要理解死锁,我们要深刻的理解这么一句话:串行与并行说的是队列,队列是用来调度和存放任务的;而同步与异步,针对的是线程。区别在于,同步会阻塞当前线程,必须要等待同步线程中的任务执行完成以后,才能继续执行下一任务;而异步线程则是不用等待。下面我们来分析几个示例,更细的理解死锁是怎么来的,又是怎么没的。
示例1:
NSLog(@"任务1 thread:%@", [NSThread currentThread]); //任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务2 thread:%@", [NSThread currentThread]); //任务2
});
NSLog(@"任务3 thread:%@", [NSThread currentThread]); // 任务3
结果:
发现后面的任务2和任务3都没有打印。
3.8dispatch_once
从函数名就知道,这个函数用于保证在应用程序执行中只执行一次指定处理。通常用于实现单例。
+ (HDFNetConfig *)sharedConfig {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
3.9dispatch_semaphore_t
当我们在处理一系列线程的时候,当线程数量达到一定量时,在以前我们会选择使用NSOperationQueue来处理并发控制,但如何在GCD中快速的控制并发呢?答案就是dispatch_semaphore。与它相关的共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
3.9.1dispatch_semaphore_create
该函数会创建一个信号量,定义如下:
dispatch_semaphore_t
dispatch_semaphore_create(long value);
传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
3.9.2dispatch_semaphore_signal
发送一个信号量,定义如下:
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
这个函数会使传入的信号量dsema的值加1。
返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。
3.9.3dispatch_semaphore_wait
等待信号,定义如下:
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个函数会使传入的信号量dsema的值减1。
这个函数的作用是这样的:
- 如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;
- 如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
当其返回值为0时表示在timeout之前,该函数所处的线程被成功唤醒。当其返回不为0时,表示timeout发生。
举个例子来说明一下信号量的工作原理:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
3.9.4示例
dispatch_semaphore_t semaphore = dispatch_semaphore_create();
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT);
for (int i = ; i < ; i++)
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
NSLog(@"%i %@",i, [NSThread currentThread]);
sleep();
dispatch_semaphore_signal(semaphore);
});
}