iOS多线程实现3-GCD
原文链接:http://www.cnblogs.com/mddblog/p/4767559.html
敲下gcd三个字母,搜狗第一条显示居然是“滚床单” ^_^
一、介绍
GCD,英文全称是Grand Central Dispatch(功能强悍的中央调度器),基于C语言编写的一套多线程开发机制,因此使用时会以函数形式出现,且大部分函数以dispatch开头,虽然是C语言的但相对于苹果其它多线程实现方式,抽象层次更高,使用起来也更加方便。
它是苹果为应对多核的并行运算提出的解决方案,它会自动利用多核进行并发处理和运算,且线程由系统自动管理(调度、运行),无需程序员参与,使用起来非常方便。
二、任务和队列
GCD有两个核心:任务和队列。
任务:要执行的操作或方法函数,队列:存放任务的集合,而我们要做的就是将任务添加到队列然后执行,GCD会自动将队列中的任务按先进先出的方式取出并交给对应线程执行。注意任务的取出是按照先进先出的方式,这也是队列的特性,但是取出后的执行顺序则不一定,下面会详细讨论。
1 任务
任务是一个比较抽象的概念,可以简单的认为是一个操作、一个函数、一个方法等等,在实际的开发中大多是以block(block使用详见)的形式,使用起来也更加灵活。
2 队列queue
- 有两种队列:串行队列和并行队列。串行队列:同步执行,在当前线程执行;并行队列:可由多个线程异步执行,但任务的取出还是FIFO的。
队列创建,根据函数第二个参数来创建串行或并行队列。
// 参数1 队列名称
// 参数2 队列类型 DISPATCH_QUEUE_SERIAL/NULL串行队列,DISPATCH_QUEUE_CONCURRENT代表并行队列
// 下面代码为创建一个串行队列,也是实际开发中用的最多的
dispatch_queue_t serialQ = dispatch_queue_create("队列名", NULL);
- 另外系统提供了两种队列:全局队列和主队列。
全局队列属于并行队列,只不过已由系统创建的没有名字,且在全局可见(可用)。获取全局队列:
/* 取得全局队列
第一个参数:线程优先级,设为默认即可,个人习惯写0,等同于默认
第二个参数:标记参数,目前没有用,一般传入0
*/
serialQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, );
主队列属于串行队列,也由系统创建,只不过运行在主线程(UI线程)。获取主队列:
// 获取主队列
serialQ = dispatch_get_main_queue();
- 关于内存
queue属于一个对象,也是占用内存的,也会使用引用计数,当向queue添加一个任务时就会将这个queue retain一下,引用计数+1,直到所有任务都完成内存才会释放。(我们在声明一个queue属性时要用strong)。
3 执行方式-2种
同步执行和异步执行。
同步执行:不会开启新的线程,在当前线程执行。
异步执行:gcd管理的线程池中有空闲线程就会从队列中取出任务执行,会开启线程。
下面为实现同步和异步的函数,函数功能为:将任务添加到队列并执行。
/* 同步执行
第一个参数:执行任务的队列:串行、并行、全局、主队列
第二个参数:block任务
*/
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 异步执行
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
注意:默认情况下,新线程都没有开启runloop,所以当block任务完成后,线程都会自动被回收,假设我们想在新开的线程中使用NSTimer,就必须开启runloop,可以使用[[NSRunLoop currentRunLoop] run]开启当前线程,这是就要自己管理线程的回收等工作。
- 另外还有两个方法,实际开发中用的并不是太多
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
加了一个barrier,意义在于:队列之前的block处理完成之后才开始处理队列中barrier的block,且barrier的block必须处理完之后,才能处理其它的block。
根据这个特性我们可以实现123456一共6个block,可以让特定几个并发执行完成之后,再并发执行剩下的block。比如123先并发,之后456再并发执行。具体代码如下(将barrier放在123与456之间即可):
- (void)barrierTest {
// 1 创建并发队列
dispatch_queue_t BCqueue = dispatch_queue_create("BarrierConcurrent", DISPATCH_QUEUE_CONCURRENT);
// 2.1 添加任务123
dispatch_async(BCqueue, ^{
NSLog(@"task1,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
sleep();
NSLog(@"task2,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
sleep();
NSLog(@"task3,%@", [NSThread currentThread]);
});
// 2.2 添加barrier
dispatch_barrier_async(BCqueue, ^{
NSLog(@"barrier");
});
// 2.3 添加任务456
dispatch_async(BCqueue, ^{
sleep();
NSLog(@"task4,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
NSLog(@"task5,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
NSLog(@"task6,%@", [NSThread currentThread]);
});
}
输出结果,为了显示效果,代码有延时操作:
::56.822 GCDTest[:] task1,<NSThread: 0x600000274780>{number = , name = (null)}
::57.827 GCDTest[:] task3,<NSThread: 0x600000271a00>{number = , name = (null)}
::59.826 GCDTest[:] task2,<NSThread: 0x600000275100>{number = , name = (null)}
::59.826 GCDTest[:] barrier
::59.827 GCDTest[:] task5,<NSThread: 0x600000271a00>{number = , name = (null)}
::59.827 GCDTest[:] task6,<NSThread: 0x608000279300>{number = , name = (null)}
::00.828 GCDTest[:] task4,<NSThread: 0x600000275100>{number = , name = (null)}
三、几种类型
很明显两种执行方式,两种队列。那么就有4种情况:串行队列同步执行、串行队列异步执行、并行队列同步执行、并行队列异步执行。哪一种会开启新的线程?开几条?是否并发?记忆起来比较绕,但是只要抓住基本的就可以,为了方便理解,现分析如下:
1)串行队列,同步执行-----串行队列意味着顺序执行,同步执行意味着不开启线程(在当前线程执行)
2)串行队列,异步执行-----串行队列意味着任务顺序执行,异步执行说明要开线程, (如果开多个线程的话,不能保证串行队列顺序执行,所以只开一个线程)
3)并行队列,异步执行-----并行队列意味着执行顺序不确定,异步执行意味着会开启线程,而并行队列又允许不按顺序执行,所以系统为了提高性能会开启多个线程,来队列取任务(队列中任务取出仍然是顺序取出的,只是线程执行无序)。
4)并行队列,同步执行-----同步执行意味着不开线程,则肯定是顺序执行
5)死锁-----程序执行不出来(死锁) ;
四、死锁举例
- 主队列死锁:
这种死锁最常见,问题也最严重,会造成主线程卡住。原因:主队列,如果主线程正在执行代码,就不调度任务;同步执行:一直执行第一个任务直到结束。两者互相等待造成死锁,示例如下:
- (void)mainThreadDeadLockTest {
NSLog(@"begin");
dispatch_sync(dispatch_get_main_queue(), ^{
// 发生死锁下面的代码不会执行
NSLog(@"middle");
});
// 发生死锁下面的代码不会执行,当然函数也不会返回,后果也最为严重
NSLog(@"end");
}
- 在其它线程死锁,这种不会影响主线程:
原因:serialQueue为串行队列,当代码执行到block1时正常,执行到dispatch_sync时,dispatch_sync等待block2执行完毕才会返回,而serialQueue是串行队列,它正在执行block1,只有等block1执行完毕后才会去执行block2,相互等待造成死锁
- (void)deadLockTest {
// 其它线程的死锁
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
// 串行队列block1
NSLog(@"begin");
dispatch_sync(serialQueue, ^{
// 串行队列block2 发生死锁,下面的代码不会执行
NSLog(@"middle");
});
// 不会打印
NSLog(@"end");
});
// 函数会返回,不影响主线程
NSLog(@"return");
}
五、常用举例
1 线程间通讯
比如,为了提高用户体验,我们一般在其他线程(非主线程)下载图片或其它网络资源,下载完成后我们要更新UI,而UI更新必须在主线程执行,所以我们经常会使用:
// 同步执行,会阻塞指导下面block中的代码执行完毕
dispatch_sync(dispatch_get_main_queue(), ^{
// 主线程,UI更新
});
// 异步执行
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程,UI更新
});
2 信号量的使用
也属于线程间通讯,下面的举例是经常用到的场景。在网络访问中,NSURLSession类都是异步的(找了很久没有找到同步的方法),而有时我们希望能够像NSURLConnection一样可以同步访问,即在网络block调用完成之后做一些操作。那我们可以使用dispatch的信号量来解决:
/// 用于线程间通讯,下面是等待一个网络完成
- (void)dispatchSemaphore {
NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
// 设置缓存策略为每次都从网络加载 超时时间30秒
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:];
dispatch_semaphore_t semaphore = dispatch_semaphore_create();
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 处理完成之后,发送信号量
NSLog(@"正在处理...");
dispatch_semaphore_signal(semaphore);
}] resume];
// 等待网络处理完成
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"处理完成!");
}
在上面的举例中
dispatch_semaphore_signal的调用必须是在另一个线程调用,因为当前线程已经dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主线程调用
3 其它常用
全局队列,实现并发:
dispatch_async(dispatch_get_global_queue(, ), ^{
// 要执行的代码
});
六、Dispatch Group调度组
使用调度组,可以轻松实现在一些任务完成后,做一些操作。比如具有顺序性要求的生产者消费者等等。
示例1 任务1完成之后执行任务2。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self groupTest];
}
- (void)groupTest {
// 创建一个组
dispatch_group_t group = dispatch_group_create();
NSLog(@"开始执行");
dispatch_async(dispatch_get_global_queue(, ), ^{
dispatch_group_async(group, dispatch_get_global_queue(, ), ^{
// 任务1
// 等待1s一段时间在执行
[NSThread sleepForTimeInterval:];
NSLog(@"task1 running in %@",[NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_global_queue(, ), ^{
// 任务2
NSLog(@"task2 running in %@",[NSThread currentThread]);
});
});
}
点击屏幕后,打印如下,可以看到任务1虽然等待了1s,任务2也不执行,只有任务1执行完毕才执行任务2.
-- ::05.317 GCDTest[:] 开始执行
-- ::06.323 GCDTest[:] task1 running in <NSThread: 0x7f8962f16900>{number = , name = (null)}
-- ::06.323 GCDTest[:] task2 running in <NSThread: 0x7f8962c92750>{number = , name = (null)}
示例2,其实示例1并不常用,真正用到的是监控多个任务完成之后,回到主线程更新UI,或者做其它事情。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self groupTest];
}
- (void)groupTest {
// 创建一个组
dispatch_group_t group = dispatch_group_create();
NSLog(@"开始执行");
dispatch_async(dispatch_get_global_queue(, ), ^{
dispatch_group_async(group, dispatch_get_global_queue(, ), ^{
// 关联任务1
NSLog(@"task1 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(, ), ^{
// 关联任务2
NSLog(@"task2 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(, ), ^{
// 关联任务3
NSLog(@"task3 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(, ), ^{
// 关联任务4
// 等待1秒
[NSThread sleepForTimeInterval:];
NSLog(@"task4 running in %@",[NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 回到主线程执行
NSLog(@"mainTask running in %@",[NSThread currentThread]);
});
});
}
点击屏幕后,打印如下,可以看到无论其它任务然后和执行,mainTask等待它们执行后才执行。
-- ::14.312 GCDTest[:] 开始执行
-- ::14.312 GCDTest[:] task3 running in <NSThread: 0x7fa8f1f0c9c0>{number = , name = (null)}
-- ::14.312 GCDTest[:] task1 running in <NSThread: 0x7fa8f1d10750>{number = , name = (null)}
-- ::14.312 GCDTest[:] task2 running in <NSThread: 0x7fa8f1c291a0>{number = , name = (null)}
-- ::15.313 GCDTest[:] task4 running in <NSThread: 0x7fa8f1d0e7f0>{number = , name = (null)}
-- ::15.313 GCDTest[:] mainTask running in <NSThread: 0x7fa8f1c13df0>{number = , name = main}
关于Dispatch对象内存管理问题
根据上面的代码,可以看出有关dispatch的对象并不是OC对象,那么,用不用像对待Core Foundation框架的对象一样,使用retain/release来管理呢?答案是不用的!
如果是ARC环境,我们无需管理,会像对待OC对象一样自动内存管理。
如果是MRC环境,不是使用retain/release,而是使用dispatch_retain/dispatch_release来管理。
测试用例github下载:https://github.com/mddios/GCDTest
iOS多线程实现3-GCD的更多相关文章
- iOS多线程知识总结--GCD
iOS多线程知识总结--GCD 1. iOS中苹果提供4钟方案来帮助我们实现多线程: (1) 纯C语言的pthread,偏底层,需要程序员手动管理线程的生命周期,基本不用. (2) OC语言的NSTr ...
- iOS多线程开发之GCD(中篇)
前文回顾: 上篇博客讲到GCD的实现是由队列和任务两部分组成,其中获取队列的方式有两种,第一种是通过GCD的API的dispatch_queue_create函数生成Dispatch Queue:第二 ...
- iOS多线程开发之GCD(死锁篇)
上篇和中篇讲解了什么是GCD,如何使用GCD,这篇文章将讲解使用GCD中将遇到的死锁问题.有兴趣的朋友可以回顾<iOS多线程开发之GCD(上篇)>和<iOS多线程开发之GCD(中篇) ...
- iOS多线程开发之GCD(中级篇)
前文回顾: 上篇博客讲到GCD的实现是由队列和任务两部分组成,其中获取队列的方式有两种,第一种是通过GCD的API的dispatch_queue_create函数生成Dispatch Queue:第二 ...
- iOS多线程开发之GCD(基础篇)
总纲: GCD基本概念 GCD如何实现 GCD如何使用 队列和任务组合 一.GCD基本概念 GCD 全称Grand Central Dispatch(大中枢队列调度),是一套低层API,提供了⼀种新的 ...
- iOS 多线程学习笔记 —— GCD
本文复制.参考自文章:iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用 ,主要为了加强个人对知识的理解和记忆,不做他用.原作者声明: 著作权声明:本文由http:// ...
- iOS 多线程:『GCD』详尽总结
本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法.这大概是史上最详细.清晰的关于 GCD 的详细讲解+总结的文章了.通过本文,您将了解到: 1. GCD 简介 2. GCD 任务和队列 ...
- iOS多线程编程之GCD的使用
什么是线程呢? 1个CPU执行的CPU命令列为一条无分叉的路径即为线程. 这种无分叉路径不止1条,存在多条时即为多线程. 什么是GCD? Grand Central Dispatch (GCD)是异步 ...
- iOS多线程NSThread和GCD
在iOS中啊 其实有多种方法实现多线程 这里只记录两个比较常用的 或者说我比较常用的 一个就是BSThread 另一个就是一听名字就比较霸气的妇孺皆知的GCD 先说一下NSThread吧 这个方式 ...
- IOS 多线程05-OperationQueue 、GCD详解
注:本人是翻译过来,并且加上本人的一点见解. 1. 开始 目前在 iOS中有两套先进的同步 API 可供我们使用:操作队列OperationQueue和 GCD .其中 GCD 是基于 C 的底层 ...
随机推荐
- 外网访问原理分析 - 每天5分钟玩转 OpenStack(105)
本节我们会将上节创建的 ext_net 连接到 router,并验证内外网的连通性. 更重要的,我们会分析隐藏在表象之下的原理. 将外网连接到 Neutron 的虚拟路由器,这样 instance 才 ...
- JavaScript框架设计(三) push兼容性和选择器上下文
JavaScript框架设计(三) push兼容性和选择器上下文 博主很久没有更博了. 在上一篇 JavaScript框架设计(二) 中实现了最基本的选择器,getId,getTag和getClass ...
- c# 我所理解的 值类型 and 引用类型
一直以来对于值类型和引用类型都只是一个模糊的概念,趁最近有空深入理解了下. 先说说值类型,在msdn上是这样介绍值类型的. 意思就是值类型直接包含值. 变量引用的位置就是值所在内存中实际存储的位置,所 ...
- 订制DOM选择器
本来是打算参考zepto.js,然后将里面想要的部分抽出来做函数,随调随用. 但后面发现这种写法重复代码太多,代码不整洁,于是就打算模仿下zepto的写法,挑出些比较实用的方法,造一下轮子. 起名叫“ ...
- 四大组件之ContentProvider
前言 ContentProvider作为Android的四大组件之一,是属于需要掌握的基础知识,可能在我们的应用中,对于Activity和Service这两个组件用的很常见,了解的也很多,但是对Con ...
- MVC利用MvcHtmlString在后台生成HTML
后台: /// <summary> /// 生成分类下拉-列表框,选中指定的项 /// </summary> /// <param name="html&quo ...
- mybatis入门基础(七)----延迟加载
一.什么是延迟加载 resultMap可以实现高级映射(使用association.collection实现一对一及一对多映射),association.collection具备延迟加载功能. 需求: ...
- 文件权限及chmod使用方法
文件权限 在linux在,由于安全控制需要,对于不同的文件有不现的权限,限制不同用户的操作权限,总共有rwxXst这一些权限,我们经常使用到的是rwx,对于文件和文件夹而言,他们代表着不同的含义 对于 ...
- MySQL char与varchar的差异
字符与字节的区别 一个字符由于所使用的字符集的不同,会并存储在一个或多个字节中,所以一个字符占用多少个字节取决于所使用的字符集 注意:char与varchar后面接的数据大小为存储的字符数,而不是字节 ...
- 8.2 使用Fluent API进行实体映射【Code-First系列】
现在,我们来学习怎么使用Fluent API来配置实体. 一.配置默认的数据表Schema Student实体 using System; using System.Collections.Gener ...