基本使用
GCD
的基本使用场景就是避免耗时任务导致界面卡顿。基本思路:
- 将耗时任务放入全局并行队列中执行。
- 执行完成之后,再切回主线程,更新界面。
- 全部采用异步执行的方式
dispatch_async
。这是多线程的核心优势:耗时任务不影响主线程响应用户操作。
// 基本使用
- (IBAction)baseButtonTouched:(id)sender {
self.baseButton.enabled = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"global_queue执行耗时任务,这里sleep 5s");
NSLog(@"%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:5];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回到main_queue,更新界面");
NSLog(@"%@", [NSThread currentThread]);
self.baseButton.enabled = YES;
});
});
}
这种场景在平时用的最多,也是GCD最有用的地方。非常简洁地避免了界面卡顿。
死锁
同步执行
dispatch_sync
会阻塞当前线程,要停下来等分派的任务执行完成才能继续。主线程队列
main queue
是一种串行队列DISPATCH_QUEUE_SERIAL
,遵循先入先出的规则。所以耗时任务会影响用户的交互操作,就是点了没反应。如果主线程中同步调用在主线程中执行的子任务,会造成“自己等自己”的现象。这个就是“死锁”。
实际执行的之后,会报错
EXC_BAD_INSTRUCTION
,现象是发生崩溃。
GCD死锁及报错提示(EXC_BAD_INSTRUCTION)
- 在主线程中应该禁止同步执行
dispatch_sync
,这是脱裤子放屁,主线程本来就是同步执行的。并且还一不小心会造成“死锁”,引起崩溃。
多读单写
可以多个读者同时读取数据,而在读的时候,不能去写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。
- 栅栏函数
dispatch_barrier_async
的含义是:(1)在栅栏之前分派的任务先执行(要全部执行完毕);(2)执行栅栏函数中的任务;(3)执行展览函数之后分派的任务。
由于读写都是耗时任务,不能影响主线程的用户界面,所以不论读写,都应该需要异步执行(就是带
_async
的分派函数)。需要一个并行队列。这里要用自定义的并行队列
dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT)
,不能用全局的匿名队列dispatch_get_global_queue
读操作,直接放入这个并行队列执行就行了。异步并行,不影响主界面,也能保证效率。
写操作,放入栅栏函数中。这里要求只有一个写操作。因为在栅栏里,并且是并行队列的情况下,无法保证执行顺序,会引起混乱。如果读写都采用串行队列,那么会导致效率低下。总之,如果有多个写操作,在栅栏里的多个写操作,需要引入其他的同步机制,保证线程安全。
这里,为了简单起见,这里只有一个写操作。所谓的“单写”。
// 多读单写,栅栏函数
- (IBAction)barrierButtonTouched:(id)sender {
// 将读写都放入同一个并行队列;注意,这里用dispatch_get_global_queue不行;
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT);
// 写之前的读操作,并发执行
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之前的读操作1。");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之前的读操作2。");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之前的读操作3。");
});
// 写操作,在栅栏函数中,单独执行
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"这是写操作,在栅栏函数中。");
[NSThread sleepForTimeInterval:1];
});
// 写之后的读操作,并发执行
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之后的读操作1。");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之后的读操作2。");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"这是写之后的读操作3。");
});
}
这里,把单独的一个写操作放在了栅栏中;写之前放了3个读操作;写之后也放了3个读操作
任务组
场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。又或者,多图下载,所有的图片组件都下载完成之后,到主线程合成一张大图。这个过程类似于Promise.all()
下载或者网络请求都是耗时任务,会影响主线程,所以需要异步执行;
各个网络请求是相互独立的,不应该等待,所以需要一个并行队列;
所有的耗时的并行任务完成之后,需要做更新界面,合并大图之类的操作。这些需要在主线程中完成。所以,这里采用任务组
dispatch_group
虽然用
dispatch_barrier_async
也能完成类似任务,不过用dispatch_group
感觉更合适
// 任务组
- (IBAction)groupButtonTouched:(id)sender {
// 网络任务开始前,修改界面
self.groupButton.enabled = NO;
NSLog(@"网络组下载任务开始了... ...");
// 子任务都放入同一个并行队列,保证效率
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT);
// 任务组
dispatch_group_t group = dispatch_group_create();
// 子任务派发;异步形式
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"完成子任务1");
});
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"完成子任务2");
});
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"完成子任务3");
});
// 子任务都完成后,更新界面等后续工作
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成所有子任务,更新界面");
self.groupButton.enabled = YES;
});
}
线程同步
初始信号量为0的dispatch_semaphore
可以用来做线程同步。
场景:任务A不需要等待;任务B第1、3部分不需要等待,第2部分需要等待任务A先完成;任务C需要等待任务B的第2部分先完成。
// 线程同步
- (IBAction)synchronizeButtonTouched:(id)sender {
// 同步任务开始前,修改界面
self.synchronizeButton.enabled = NO;
// 一开始,AB都没有完成,没有信号
dispatch_semaphore_t semaphoreA = dispatch_semaphore_create(0);
dispatch_semaphore_t semaphoreB = dispatch_semaphore_create(0);
// ABC同时开始;异步执行的并行队列,保证效率
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务A不需要等待,开始...");
sleep(2);
NSLog(@"任务A完成,设置semaphoreA");
dispatch_semaphore_signal(semaphoreA);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务B第1部分不需要等待,开始...");
sleep(1);
NSLog(@"任务B第1部分完成,等待任务A的完成信号semaphoreA");
dispatch_semaphore_wait(semaphoreA, DISPATCH_TIME_FOREVER);
NSLog(@"收到任务A完成的信号,任务B第2部分开始...");
sleep(2);
dispatch_semaphore_signal(semaphoreB);
NSLog(@"任务B第2部分完成,设置semaphoreB;第3部分,开始...");
sleep(6);
NSLog(@"任务B第3部分完成。耗时较长,估计排最后面了。");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"要等任务B第2部分完成,任务C开始不了!!!!");
dispatch_semaphore_wait(semaphoreB, DISPATCH_TIME_FOREVER);
NSLog(@"任务B第2部分终于来了,任务C开始...");
[NSThread sleepForTimeInterval:0.5];
NSLog(@"任务C完成!");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"任务C负责更新界面,不管任务B第3部分!!!!");
self.synchronizeButton.enabled = YES;
});
});
}
线程安全
初始信号量为1的dispatch_semaphore
可以用来当做锁,对只能独占访问的关键资源进行保护。
场景:假设银行里本来有钱1000;现在有4个人同时来取钱。前面一些操作是并发的,比如输入密码之类的。这里用sleep(5)来模拟。
取钱过程,需要独占访问,所以用一个初始信号量为1的dispatch_semaphore
进行保护;
- 查询余额;
- 取钱过程,这里用sleep(2)来模拟;
- 取钱,同时查询余额;
// 线程安全
- (IBAction)safeButtonTouched:(id)sender {
// 值为1的semaphore相当于锁
dispatch_semaphore_t lockSemaphore = dispatch_semaphore_create(1);
// 钱只有一份,是大家公用的,需要加锁,保证线程安全
__block NSInteger money = 1000;
// 10个人同时来取钱
for (int i = 0; i < 4; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"第%d个人开始操作,初始金额:%ld===", (i + 1), money);
sleep(5);
// 访问关键资源,用锁保护
dispatch_semaphore_wait(lockSemaphore, DISPATCH_TIME_FOREVER);
NSLog(@"第%d个人开始取钱;当前余额:%ld...", (i + 1), money);
sleep(2);
money -= 100; // 每人每次取10元
NSLog(@"第%d个人取钱完毕;当前余额:%ld", (i + 1), money);
dispatch_semaphore_signal(lockSemaphore);
});
}
}
如果不保护,那么金额完全就乱了。(就是注释掉有关dispatch_semaphore
的几句)
signal的调用次数一定要大于等于wait的调用次数,否则导致崩溃。
dispatch_semaphore使用崩溃问题
延时执行
延时执行,我们往往会想到sleep()
函数。这样做虽然能达到目的,不过这会阻塞当前线程,效率低下。如果在主线程使用sleep()
,那么在这段延时期间,界面将失去响应。
dispatch_after
就是专门做延时执行的,异步的,不会阻塞当前线程,效果很好。
- (IBAction)delayButtonTouched:(id)sender {
NSLog(@"延时执行开始....");
self.delayButton.enabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"延时执行结束");
self.delayButton.enabled = YES;
});
}
单次执行
在语义上有单个的业务,采用单例模式是非常方便的。比如,常用的个人信息,就可以保留在一个单例之中。
那么如何保证单例的实现呢?
dispatch_once
顾名思义,就是只执行一次,这和单例的语义一致,所以非常方便。
新建一个单例SingleDemo
,其中获取实例的方法如下:
// 获取单例对象
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static id instance = nil;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
所有的单例都可以这么写,直接固定为一个宏也可以。
可以测试一下,是否只有一个实例:
// 单次执行
- (IBAction)onceButtonTouched:(id)sender {
SingleDemo *single1 = [SingleDemo sharedInstance];
NSLog(@"对象single1地址:%@", single1);
SingleDemo *single2 = [SingleDemo sharedInstance];
NSLog(@"对象single2地址:%@", single2);
SingleDemo *single3 = [SingleDemo sharedInstance];
NSLog(@"对象single3地址:%@", single3);
}
定时器
大多数时候,用NSTimer
就可以了。不过NSTimer
和RunLoop
相关联,定时精度受影响。所以有些时候需要用到GCD
定时器。
iOS - GCD 实现定时器、倒计时
iOS GCD定时器使用及封装
-
Xcode
对于GCD
有代码提示,很方便
-
timer
不能是临时变量,需要长久存在,不然block
不会执行。比如作为属性:
// 定时器
@property (strong, nonatomic) dispatch_source_t timer;
- 可以将定时器设置在并行队列中,不会影响主线程的界面
// 定时器
- (IBAction)timeButtonTouched:(id)sender {
__block NSInteger index = 0;
// 将定时器设置在并行队列,不影响主线程的界面
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timer, ^{
index++;
NSLog(@"定时器任务第%ld次执行", (long)index);
});
dispatch_resume(self.timer);
}
- 设置之后,会马上执行一次
block
,不会等下一个周期。
参考文章
2019 iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatc