目录(GCD):
- 关键词
- 混淆点
- 场景应用
- 总结
1. 关键词
- 线程概念: 独立执行的代码段,一个线程同时间只能执行一个任务,反之多线程并发就可以在同一时间执行多个任务。
- 一个线程中的任务是串行执行的;
- 一个线程中执行多个任务,只能一个一个的按顺序执行;
- 因此比较耗时的操作应该放在“非主线程”.
- 任务:执行的操作(
Block
里的代码) - 同步异步:
- 同步在当前线程中执行,任务会立即执行,它会阻塞当前线程并等待
Block
中的任务执行完毕,然后当前线程才会继续运行,就是在发出一个功能调用时,在没有得到结果之前,该调用就不继续往下运行(调用)。是一定不会开新线程的,也就是必须一件一件事做,等前一件做完了才能做下一件事。(一个线程,当前线程。); - 异步:可以开新线程,可以在新线程内执行任务,并不意味着一定就会开新线程;任务不会立即执行,当前线程会直接往下执行,它不会阻塞当前线程。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的任务在完成后,通过状态、通知和回调来通知调用者。(多个线程,开辟出来的新线程。);
- 同步(sync) 和 异步(async) 的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕。
- 同步在当前线程中执行,任务会立即执行,它会阻塞当前线程并等待
- 队列
- 串行队列:放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行,队列中的任务一个一个顺序执行。(一个任务执行->等待返回结果->下一个任务执行);
- 并行队列:放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。有高、默认、低和后台4个优先级。并发功能只有在异步(
dispatch_async
)函数下才有效. - 注意两个非常常用的特殊队列:
- 主队列
UI 操作放在主队列中执行
跟主线程相关联的队列!
主队列是 GCD 自带的一种特殊的串行队列,注意是串行
主队列中的任务都会在主线程中执行
获取主队列
* 全局并发队列
一般情况下,并发任务都可以放在全局并发队列中
获取全局并发队列
- 创建队列方法
1.使用dispatch_queue_create
函数创建队列
串行队列
//(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("serial_queue", NULL);
并发队列:
dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
2.使用主队列(跟主线程相关联的队列)
主队列是GCD自带的一种特殊的串行队列:放在主队列中的任务,都会放到主线程中执行。可以使用dispatch_get_main_queue()
获得系统提供的主队列:
dispatch_queue_t queue = dispatch_get_main_queue();
3.使用dispatch_get_global_queue
获得全局并发队列。
GCD默认已经提供了全局的并发队列,供整个应用使用,可以无需手动创建。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- Critical Section (临界区)
简而言之就是两个或多个线程不能同时执行一段代码去操作一个共享的资源 - Race Condition (竞态条件)
这种情况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,
竞态条件可导致无法预测的行为,例如程序的并发任务执行的确切顺序 - Deadlock (死锁)
所谓的死锁是指两个(或多个)线程都卡住了,都在等待对方完成后执行,
第一个不能完成是因为在等待第二个的完成,但第二个也不能完成,
是因为它在等待第一个完成 - 多线程安全:当某线程访问一个数据之前就要给数据加锁,让其不被其他的线程所修改。
互斥锁:
互斥锁的使用前提:多条线程抢夺同一块资源的时候使用
@synchronized(锁对象) {
// 需要锁定的代码
};
```
如果给数据加了锁,就等于将这些异步的子线程变成同步的了,这也叫做线程同步技术。
互斥锁的优缺点:
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
2. 混淆点
- 队列和线程区别:
简单来说,队列就是用来存放任务的“暂存区”,而线程是执行任务的路径,GCD将这些存在于队列的任务取出来放到相应的线程上去执行,而队列的性质决定了在其中的任务在哪种线程上执行。dispatch_asyn
和dispatch_sync
添加任务到dispatch
队列时,是否创建线程呢,那么创建线程是创建一个呢还是多个呢?-
dispatch_sync
添加任务到队列,不会创建新的线程,都是在当前线程中处理的。无论添加到串行队列里或者并行队列里,都是串行效果,因为这个方法是等任务执行完成以后才会返回。 -
dispatch_async
添加任务到-
mainQueue
不创建线程,在主线程中串行执行 -
globalQueue
和 并行队列:根据任务系统决定开辟线程个数 - 串行对列:创建一个线程:串行执行。
-
-
-
dispatch_sync()
与dispatch_async()
的区别:-
dispatch_async()
函数是非同步的,它只负责将任务添加到队列中,并不在乎添加到队列中的任务是否处理完成。而相对于dispatch_async()
-
dispatch_sync()
函数是同步的,dispatch_sync()
函数不但负责将任务添加到队列中,还要等待添加的任务执行完成再返回,在此过程调用dispatch_sync()
函数所在的线程被挂起,直到dispatch_sync()
函数返回,所在线程恢复,注意**是调用dispatch_sync()
函数的线程被挂起。
-
- 理解
dispatch_sync
与dispatch_async
的工作流程
dispatch_sync(queue,block) 做了两件事:
1)将block添加到queue队列中
2)阻塞调用线程,等待block()执行结束,回到调用线程。
dispatch_async(queue,block) 做了两件事
1)将block添加到queue队列;
2)直接回到调用线程(不阻塞调用线程)。
当在main_thread中调用dispatch_sync 时:
1)main_thread被阻塞,无法继续执行;
2)同步派发sync导致block()需要在main_thread中执行结束才回返回;
3)而此时main_thread被阻塞,二者相互等待,死锁。
- 进一步解释死锁
函数比较重要的一个问题就是死锁,为什么会出现死锁的情况呢?
比如多我们有一个串行队列,并且dispatch_sync()
函数的调用也是在该队列中,这样串行队列的线程在调用dispatch_sync()
函数的时候被挂起,而线程被挂起之后dispatch_sync()
函数添加的任务一直得不到线程的处理,一直不能返回,所以线程将一直处于被挂起的状态。出现这种状况的核心就是:调用dispatch_sync()
函数的线程(注意是线程,而不是队列,并行队列有多个线程可能并不会发生这种状况,除非调用函数的任务和函数追加的任务被分配到并行队列中同一线程中去)和处理函数追加的任务的线程是同一个线程。此时就会发生死锁。
强调一点,Dispatch Queue
队列并不是指我们印象中的线程队列,它是任务队列,它只负责任务的管理,并不进行任务的执行操作,任务的执行是由Dispatch Queue
分配的线程来完成的
避免死锁的方法是在使用dispatch_sync
执行任务时,传入参数的队列不要和当前线程的队列是一样的。
下面用代码结合文字说一下:
dispatch_sync和 dispatch_async需要两个参数,一个是队列,一个是block,它们的共同点是block都会在你指定的队列上执行(无论队列是并行队列还是串行队列),不同的是dispatch_sync会阻塞当前调用GCD的线程直到block结束,而dispatch_async异步继续执行。例子如下面:
-(void)func{
dispatch_async(someQueue, ^{
//do some work.
NSLog(@"Here 1.");
});
NSLog(@"Here 2.");
}
因为dispatch_async异步非阻塞,所以Here 1.和Here 2.的打印顺序不确定;
-(void)func{
dispatch_sync(someQueue, ^{
//do some work.
NSLog(@"Here 1.");
});
NSLog(@"Here 2.");
}
因为dispatch_sync阻塞当前操作知道block返回,所以打印顺序一定是Here 1. 然后再打印Here 2.
3. 场景应用
- 用 GCD 的快速迭代
第一个参数: 迭代次数,第二个参数: 线程队列(并发队列) ,第三个参数: index 索引
dispatch_apply(10000, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"GCD- %zd -- %@", index, [NSThread currentThread]);
});
- GCD 计时器应用
NSTimer 的定时器是在 RunLoop 中实现的,由于RunLoop在处理各种任务,所以会造成计时器不够准确,有时候会相对慢一些,有没有什么方法会让计时变得准确?有,使用 GCD 的计时器方法会让计时器变得相对准确,而且GCD不受RunLoop的 Mode 影响。
我们需要做的是,选择其队列类型,这里我选择的是全局队列。
dispatch Queue :决定了将来回调的方法在哪里执行。
dispatch_source_t timer 是一个OC对象
DISPATCH_TIME_NOW 第二个参数:定时器开始时间,也可以使用如下的方法,在Now 的时间基础上再延时多长时间执行以下任务。
dispatch_time(<#dispatch_time_t when#>, <#int64_t delta#>)
intervalInSeconds 第三个参数:定时器开始后的间隔时间(纳秒 NSEC_PER_SEC)
leewayInSeconds 第四个参数:间隔精准度,0代标最精准,传入一个大于0的数,代表多少秒的范围是可以接收的,主要为了提高程序性能,积攒一定的时间,Runloop执行完任务会睡觉,这个方法让他多睡一会,积攒时间,任务也就相应多了一点,而后一起执行
// 全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 创建一个 timer 类型定时器 ( DISPATCH_SOURCE_TYPE_TIMER)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器的各种属性(何时开始,间隔多久执行)
// GCD 的时间参数一般为纳秒 (1 秒 = 10 的 9 次方 纳秒)
// 指定定时器开始的时间和间隔的时间
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0);
// 任务回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"-----定时器-------");
});
// 开始定时器任务(定时器默认开始是暂停的,需要复位开启)
dispatch_resume(timer);
- GCD实现验证码倒计时按钮以例:
// 开启倒计时效果
(IBAction)openCountdown:(id)sender {
__block NSInteger time = 59; //倒计时时间
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer,DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC, 0); //每秒执行
dispatch_source_set_event_handler(timer, ^{
if(time <= 0){ //倒计时结束,关闭
dispatch_source_cancel(timer);
dispatch_async(dispatch_get_main_queue(), ^{
//设置按钮的样式
[self.openSeconds setTitle:@"重新发送" forState:UIControlStateNormal];
self.timeLabel.text = @"开始";
self.openSeconds.userInteractionEnabled = YES;
});
}else{
int seconds = time % 60;
dispatch_async(dispatch_get_main_queue(), ^{
//设置label读秒效果
self.timeLabel.text = [NSString stringWithFormat:@"重新发送(%.2d)",seconds];
[self.openSeconds setTitle:@"已发送" forState:UIControlStateNormal];
// 在这个状态下 用户交互关闭,防止再次点击 button 再次计时
self.openSeconds.userInteractionEnabled = NO;
});
time--;
}
});
dispatch_resume(timer);
}
- 循环执行任务
dispatch_apply类似一个for循环,并发的执行每一项。所有任务结束后,dispatch_apply才会返回,会阻塞当前线程。如果传入队列是串行队列,要注意防止死锁现象的发生。
循环执行任务,任务的顺序是无序列的并且会堵塞当前的线程。 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
count: 循环执行次数
queue: 队列,可以是串行队列或者是并行队列
block: 任务
dispatch_apply(count, queue, ^(size_t i) {
NSLog(@"%zu %@", i, [NSThread currentThread]);
});
- 延时执行
iOS常见的延时执行有2种方式
调用NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 2秒后再调用self的run方法
使用GCD函数延迟执行:dispatch_after
不需要再写方法,且它还传递了一个队列,我们可以指定并安排其线程。如果队列是主队列,那么就在主线程执行,如果队列是并发队列,那么会新开启一个线程,在子线程中执行。
延迟执行, 这段代码将会在2秒后将任务插入RunLoop当中
dispatch_queue_t queue= dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"主队列--延迟执行------%@",[NSThread currentThread]);
});
- 子线程与主线程的通信:
例子:从网络加载图片(在子线程),加载完成就更新UIView(在主线程)。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//加载图片
NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
UIImage *imageFromData = [UIImage imageWithData:dataFromURL];
dispatch_async(dispatch_get_main_queue(), ^{
//加载完成更新view
UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];
});
});
- dispatch_once
需求点:用于在程序启动到终止,只执行一次的代码。此代码被执行后,相当于自身全部被加上了注释,不会再执行了。为了实现这个需求,我们需要使用dispatch_once
让代码在运行一次后即刻被“雪藏”。
//使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码,这里默认是线程安全的:不会有其他线程可以访问到这里
});
单例模式:可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问,从而方便地控制了实例个数,并节约系统资源
使用场合
在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次)
static MBGlobalTool *_instance = nil;
(instancetype)sharedInstance
{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init] ;
}) ;
return _instance ;
}
- 队列组-dispatch_group
需求点:执行多个耗时的异步任务,但是只能等到这些任务都执行完毕后,才能在主线程执行某个任务。为了实现这个需求,我们需要让将这些异步执行的操作放在dispatch_group_async
函数中执行,最后再调用dispatch_group_notify
来执行最后执行的任务。
首先:分别异步执行2个耗时的操作
其次:等2个异步操作都执行完毕后,再回到主线程执行操作
如果想要快速高效地实现上述需求,可以考虑用队列组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});
- 栅栏-dispatch_barrier
需求点:虽然我们有时要执行几个不同的异步任务,但是我们还是要将其分成两组:当第一组异步任务都执行完成后才执行第二组的异步任务。这里的组可以包含一个任务,也可以包含多个任务。
为了实现这个需求,我们需要使用dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
在两组任务之间形成“栅栏”,使其“下方”的异步任务在其“上方”的异步任务都完成之前是无法执行的。
dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"----任务 1-----"); });
dispatch_async(queue, ^{
NSLog(@"----任务 2-----"); });
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier-----");
});
dispatch_async(queue, ^{
NSLog(@"----任务 3-----");
});
dispatch_async(queue, ^{
NSLog(@"----任务 4-----"); });
4. 总结
同步异步函数的作用:将任务添加到队列中
队列作用:决定任务执行的顺序,先添加的先执行,最后添加的任务最后执行
同步函数:堵塞当前线程;需要等待任务结束才能返回;在当前线程中进行;无开辟线程的权限
异步函数:不会堵塞当前线程;不需要等待任务;有开辟线程的权限
队列和任务关系;任务放在队列里
任务跟线程关系:任务需要线程来执行;
线程跟队列关系:一个队列里可能有好多线程,在一个线程内可能也有多个队列;在某个线程里创建队列,那么这个队列是属于这个线程的;
主队列和主线程的关系:主队列是主线中的一个串行队列。所有的和UI的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI,每一个应用程序只有唯一的一个主队列用来update UI。如果在主线里创建了一个自定义的队列,那么这个队列也就属于主线程的队列。
注意重点:果在主线里创建了一个自定义的队列,且如果主线程在主队列中被堵塞了,那么主线程会跑到这个自定义队列里看看有没有任务,如果有就执行。
如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务
具体场景解释:
同步主队列死锁原因:
在主线程里,开启同步任务,并打算使用主队列,当开始执行同步函数的时候,这时候发生了什么:
- 主队列会把同步函数任务放到主队列的最前面也就是最先执行的地方
- 同步函数堵塞当前的线程也就是主线程,并把block中的任务添加到主队列,这时候block中的任务跟同步任务都在同一个线程、同一个队列,block中的任务排在同步任务的后面,等待前面任务(同步任务)的执行。
- 同步函数的特点是必须等待block中的任务执行完才能返回,但是同步任务和block中的任务都在主队列里,根据先进先执行(FIFO)的原则,会发生这样的无限循环现象:同步任务等待block任务执行完,但是同步任务又排在了block任务的前面,block任务不能执行,那就等于同步任务也不能执行完成,所以产生死锁现象。
同步串行队列不会发生死锁原因:
在主线程里,开启同步任务,并自定义串行队列,当开始执行同步函数的时候,这时候发生了什么:
- 主队列会把同步函数任务放到主队列的最前面也就是最先执行的地方
- 同步函数堵塞当前的线程也就是主线程,并把block中的任务添加到自定义串行队列,这时候,在主线程也就有了两个队列,block中的任务跟同步任务都在同一个线程,但是不在同一个队列。
- 主线程在主队列被同步函数堵塞,会跑的它的其他队列里,看看有没有要执行的任务。所以它会去自定义队列里去执行任务。
- 同步函数的特点是必须等待block中的任务执行完才能返回,自定义队列里的任务已被执行完,同步函数也就执行完返回主线程。
重点:虽然主队列中的主线程被堵塞了,但是主线程可以去执行其他属于主线程的队列的任务。