1. 进程和线程的相关概念
1.1什么是进程
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,比如同时打开QQ、Xcode,系统就会分别启动2个进程,通过“活动监视器”可以查看Mac系统中所开启的进程.要是同时开启两个播放器,那么就是开启了两个进程。
1.2什么是线程
1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程),线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行 比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行.
1.3线程的串行
1个线程中任务的执行是串行的,如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,1个线程只能执行1个任务,比如在1个线程中下载3个文件(分别是文件A、文件B、文件C),a->b->c,c最后执行完。
1.4 进程和线程的比较
- 线程是CPU调用(执行任务)的最小单位。
- 进程是CPU分配资源和调度的单位。
- 一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程。
- 同一个进程内的线程共享进程的资源。
2. 多线程的相关概念
2.1什么是多线程
1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。
多线程技术可以提高程序的执行效率,比如在一个进程中同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C) 。
2.2 多线程的原理
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况? CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源每条线程被调度执行的频次会降低(线程的执行效率降低)
多核时代改变了这一情况,但是GCD等函数帮助我们不需管理核与核之间的切换。
2.3 多线程的优缺点
多线程的优点
- 能适当提高程序的执行效率
- 能适当提高资源利用率(CPU、内存利用率)
多线程的缺点
- 开启线程需要占用一定的内存(栈)空间(默认情况下,主线程占用1M,子线程占用512KB,90毫秒的创建时间),如果开启大量的线程,会占用大量的内存空间,降低程序的性能 线程越多,CPU在调度线程上的开销就越大
- 程序设计更加复杂:比如线程之间的通信、多线程的数据共享
2.4 多线程在iOS开发中的应用
主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。
主线程的主要作用
- 显示\刷新UI界面
- 处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意:别将比较耗时的操作放到主线程中。
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验。
- 当在主线程执行一个for循环10000次,想挪动
textfiled
的内容,但是无法办到,并且点击事件已经放到主线程里了,只等for循环结束就执行这个操作。
3. NSThread的使用
3.1 创建NSThread
以下几个开辟线程的方法,一个NSThread对象就代表一条线程。
创建、启动线程
/** target: 目标对象 self selector: run 方法选择器 - 调用的方法 object : 前面调用方法需要传递的参数 */ NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"111"]; thread.name = @"name1"; thread.threadPriority = 0.9; [thread start]; //从当前线程中分离出子线程 - 没有返回值 [NSThread detachNewThreadSelector:@selector(run2:) toTarget:self withObject:@"222"]; [self performSelector:@selector(run2:) withObject:@"开启后台线程"];复制代码
NSThread的相关用法
+ (NSThread *)mainThread;
- 获得主线程-(BOOL)isMainThread;
- 是否为主线程+ (BOOL)isMainThread;
- 是否为主线程NSThread *current = [NSThread currentThread];
- 获得当前线程
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"111"]; threadA.name = @"nameA"; threadA.threadPriority = 0.9; [threadA start]; NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"111"]; threadB.name = @"nameB"; threadB.threadPriority = 0.5; [threadB start]; NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"111"]; threadC.name = @"nameC"; threadC.threadPriority = 0.1; [threadC start];复制代码
- (void)run:(NSString *)str{ for (NSInteger i = 0; i<100; i++) { NSLog(@"%zd --- %@",i,[NSThread currentThread].name); } }OC知识点合集[533:289192] 0 --- nameAOC知识点合集[533:289192] 1 --- nameA...OC知识点合集[533:289192] 20 --- nameAOC知识点合集[533:289193] 0 --- nameBOC知识点合集[533:289194] 0 --- nameC...OC知识点合集[533:289193] 99 --- nameBOC知识点合集[533:289194] 1 --- nameC复制代码
线程的调度优先级:调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- 在多条线程中比较有用
设置线程的名字 - thread.name = @"name1";
performSelectorInBackground
和detachNewThreadSelector
的创建方法优缺点 优点:简单快捷 缺点:无法对线程进行更详细的设置
3.2 NSThread线程间通信
- performSelectorOnMainThread
- performSelector:target onThread:
- (void)threadCommunication{ UIImageView *imageView = [[UIImageView alloc] init]; imageView.frame = CGRectMake(100, 100, 100, 100); [self.view addSubview:imageView]; self.imageV = imageView; NSURL *url = [NSURL URLWithString:@"http://img3.duitang.com/uploads/blog/201402/05/20140205123502_WyAa4.thumb.700_0.jpeg"]; //也可以用NSDate获取当前时间 CFTimeInterval是一个绝对值,从2001年开始算起 CFTimeInterval start = CFAbsoluteTimeGetCurrent(); NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; CFTimeInterval end = CFAbsoluteTimeGetCurrent(); NSLog(@"end - start = %f",end - start); /** 回到主线程刷新UI MainThread:选择器 - 回到主线程调用哪个方法 Object: 前面方法需要传递的参数 UntilDone: 是否等待,有可能在performSelectorOnMainThread这个消息下还有代码,这个等待为true则等执行完再执行下面代码 */// 两个都是给imageView赋值 ,但是下面的少些一步 ,因为UIImageView中的image是用@property修饰,有set方法// [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:NO];// [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO]; [self.imageV performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO]; NSLog(@"%@",[NSThread currentThread]);}- (void)showImage:(UIImage *)image{ self.imageV.image = image;}复制代码
一些用法
//创建线程 NSThread通过alloc init开辟空间的话,要去自定义的头文件找指定的任务 DXThread *thread = [[DXThread alloc] init]; //启动线程 [thread start];复制代码
4.线程的安全
4.1多线程的安全隐患
资源共享 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如多个线程访问同一个对象、同一个变量、同一个文件,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。最经典的例子就是银行存取钱,要是同时操作的话,原有1000元,一边从支付宝存1000元,一遍取500元,最后返回500元了。
4.2如何解决
互斥锁使用格式 @synchronized(锁对象) { // 需要锁定的代码 } 注意:锁定1份代码只用1把锁,用多把锁是无效的。 互斥锁的优缺点 优点:能有效防止因多线程抢夺资源造成的数据安全问题 缺点:需要消耗大量的CPU资源
互斥锁的使用前提:多条线程抢夺同一块资源 相关专业术语:线程同步,多条线程按顺序地执行任务,互斥锁,就是使用了线程同步技术。
4.3死锁
4.3.1死锁简单介绍
死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
发生死锁的情况一般是两个对象的锁相互等待造成的。 那么为什么会产生死锁呢?有3个原因:第一,因为系统资源不足;第二,进程运行推进的顺序不合适(程序员的bug啦);第三,资源分配不当;第四, 占用资源的程序崩溃 。
产生死锁的条件有四个: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在相互等待,造成了无法执行的情况。
4.3.2死锁的解决方案
避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,在访问B和C。
预防 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配 。因此,对资源的分配要给予合理的规划。
一、有序资源分配法 这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程: 1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完; 2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R1,R2;若采用动态分配有可能形成环路条件,造成死锁。 采用有序资源分配法:R1的编号为1,R2的编号为2; PA:申请次序应是:R1,R2 PB:申请次序应是:R1,R2 这样就破坏了环路条件,避免了死锁的发生
二、银行算法 避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法: 该算法需要检查申请者对资源的最大需求量,如果系统现存的各类资源可以满足申请者的请求,就满足申请者的请求。 这样申请者就可很快完成其计算,然后释放它占用的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。
避免死锁 最具有代表性的避免死锁的算法,是Dijkastra的银行家算法,这是由于该算法能用于银行系统现金贷款的发放而得名的,为实现银行家算法,系统中必须设置若干数据结构. 1)可利用资源向量Available 是个含有m个元素的数组,其中的每一个元素代表一类可利用的资源数目。如果Available[j]=K,则表示系统中现有Rj类资源K个。 2)最大需求矩阵Max 这是一个n×m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i,j]=K,则表示进程i需要Rj类资源的最大数目为K。 3)分配矩阵Allocation 这也是一个n×m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的 数目为K。 4)需求矩阵Need。 这也是一个n×m的矩阵,用以表示每一个进程尚需的各类资源数。如果Need[i,j]=K,则表示进程i还需要Rj类资源K个,方能完成其任务。 Need[i,j]=Max[i,j]-Allocation[i,j]
排除方法 1、撤消陷于死锁的全部进程; 2、逐个撤消陷于死锁的进程,直到死锁不存在; 3、从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失; 4、从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。 http://zhidao.baidu.com/link?url=ITQn-LvJKNXJg6tbMdgJlM1oFegZptPwakvMrNkYPJWHK6tl9cpvCIqIrHCOtmyLcPaggkvvK_lvPuUdaw4zrflgPlPioVwcY9tAdDjFc5O
5.线程间的通信
5.1 简单说明
线程间通信:在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信.
线程间通信的体现:
- 1个线程传递数据给另1个线程
- 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
线程间通信常用方法
//aSelector : 回到主线程调用这个方法- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;//thread: 传入一个线程去执行任务aSelector- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait;复制代码
![示例](./屏幕快照 2018-05-04 下午1.30.21.png)
[self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];复制代码
![NSObject写的分类](./屏幕快照 2018-05-04 下午2.56.37.png)
意义就是凡是对象都可以调用这些方法
6. GCD
6.1简单介绍
6.1.1 什么是GCD?
全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”,纯C语言,提供了非常多强大的函数。在iOS6之后纳入ARC的内存管理范畴,不需要对create和retain函数做realease操作,就像对待oc对象一样对待就好。
6.1.2.GCD的优势
-
GCD是苹果公司为多核的并行运算提出的解决方案。
-
GCD会自动利用更多的CPU内核(比如双核、四核)。
-
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
-
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。
6.1.3 核心概念-任务和队列
GCD中有2个核心概念:
- 任务:执行什么操作。
- 队列:用来存放任务,调度任务,安排任务在哪个线程中执行。
GCD的使用就2个步骤:
- 定制任务
- 确定想做的事情
- 将任务添加到队列中
- GCD会自动将队列中的任务取出,放到对应的线程中执行
GCD只支持FIFO(First In First Out )的队列原则(两个口);而像栈这种数据结构就是先进后出,后进先出(一个口)。
6.1.3.1 执行任务
GCD中有2个用来执行任务的函数:
- 用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- 用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
说明:这两个函数的作用就是把右边的参数(封装好任务)提交给左边的参数(添加任务到队列中,队列调度任务)进行执行。 参数说明:
- queue:队列
- block:封装任务
同步和异步的区别:
- 同步:只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步:可以在新的线程中执行任务,具备开启新线程的能力。
还有一种方式能开启异步线程,跟dispatch_async
区别就是封装的方式不一样,其他都是一样的
{ /* - 队列 - 给后面函数传递的参数 - 要调用的函数名称 */ dispatch_async_f(dispatch_get_global_queue(0, 0), NULL, task); }void task(void *parm){ NSLog(@"%s",__func__);}复制代码
6.1.3.2 队列的类型
GCD的队列可以分为2大类型
-
并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_async)函数下才有效,并且只有在异步函数下才有效。
- 那么在异步函数和并发队列组合下会开启子线程
-
串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
几个容易混淆的术语
有4个术语比较容易混淆:同步、异步、并发、串行。
-
同步和异步决定了要不要开启新的线程,它指的是GCD的两个函数
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
-
并发和串行并不是指任务的执行方式 - 而是指的队列
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
6.1.4 串行队列
GCD中获得串行有2种途径
- 使用
dispatch_queue_create
函数创建串行队列
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
示例:
dispatch_queue_t queue = dispatch_queue_create("wendingding", DISPATCH_QUEUE_SERIAL); dispatch_release(queue); // 非ARC需要释放手动创建的队列复制代码
- 使用主队列(跟主线程相关联的队列)
主队列是GCD自带的一种特殊的串行队列,跟主线程相关联的队列,放在主队列中的任务,都会放到主线程中执行。
使用dispatch_get_main_queue()
获得主队列
示例:
dispatch_queue_t queue = dispatch_get_main_queue();
6.1.5 并发队列
GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。
使用dispatch_get_global_queue
函数获得全局的并发队列
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);
示例:
//获得全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); /** - 第一个参数 long identifier: 优先级 - DISPATCH_QUEUE_PRIORITY_DEFAULT:默认 - DISPATCH_QUEUE_PRIORITY_BACKGROUND - 最低 - 第二个参数 unsigned long flags: 给未来使用,一般传个0 */复制代码
说明:全局并发队列的优先级
define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高 define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中) define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低 define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台复制代码
- (void)viewDidLoad { [super viewDidLoad]; //1.获得全局的并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //2.添加任务到队列中,就可以执行任务 //异步函数:具备开启新线程的能力 dispatch_async(queue, ^{ NSLog(@"下载图片1----%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"下载图片2----%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"下载图片2----%@",[NSThread currentThread]); }); //打印主线程 NSLog(@"主线程----%@",[NSThread mainThread]);//总结:同时开启三个子线程 }复制代码
小结: 说明:异步函数具备开启线程的能力,而开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)。
补充:
- 凡是函数中,各种函数名中带有create\copy\new\retain等字眼,都需要在不需要使用这个数据的时候进行release。
- GCD的数据类型在ARC的环境下不需要再做release。
- CF(core Foundation)的数据类型在ARC环境下还是需要做release。
- 两种获得并发队列的方法区别:一种是创建并发队列,一种是拿系统的全局并发队列来用。
- GCD的异步并发队列并不是我们有多少个任务就有开启多少个线程,它不受我们控制,有可能10个任务开10个子线程,也有可能10个任务开3个子线程,具体是由系统决定。
6.2 几种搭配用法
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程;串行执行任务 | 没有开启新线程;同步执行任务 | 死锁 |
异步(sync) | 开启新线程;并发执行任务 | 开启一条新线程;同步执行任务 | 在主线程中同步执行任务 |
- 不再主线程中的异步函数才会开启子线程。
- 开启几条子线程就要看队列的类型。
- 串行队列1条线程,并发队列多条线程。
- 异步函数+并发队列 - 会开启子线程,并并发执行任务
- (void)asycnConcurrent{ //1. 创建队列 /** - 第一个参数 const char * _Nullable label: C语言的字符串,标识这个线程(可以有多个队列,需要区分) - 第一个参数 dispatch_queue_attr_t _Nullable attr: 队列的类型 - DISPATCH_QUEUE_CONCURRENT - 并发队列 - DISPATCH_QUEUE_SERIAL - 串行队列 */ dispatch_queue_t queue = dispatch_queue_create("com.ipa361.text1", DISPATCH_QUEUE_CONCURRENT); NSLog(@"---start-----"); //2. 封装任务 /** - 第一个参数 队列 - 第一个参数 要执行的任务(本质就是一个匿名函数) - DISPATCH_QUEUE_CONCURRENT - 并发队列 - DISPATCH_QUEUE_SERIAL - 串行队列 */ dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 1 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 2 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 3 ---%@",[NSThread currentThread]); }); NSLog(@"---end-----"); /** ---start----- ---end----- {number = 2,name = (null)}; {number = 4,name = (null)}; {number = 3,name = (null)}; */}复制代码
- 异步函数+串行队列 - 会开启子线程,开一条线程,队列中的任务是串行执行的
- 一般来说串行队列不需要赋值任何属性,所以可以传空值(NULL)
- (void)asycnSerial{ dispatch_queue_t queue = dispatch_queue_create("com.ipa361.text1", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ NSLog(@"asycnSerial 1 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnSerial 2 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnSerial 3 ---%@",[NSThread currentThread]); }); /** {number = 2,name = (null)}; {number = 2,name = (null)}; {number = 2,name = (null)}; */}复制代码
- 同步函数+并发队列 - 不会开线程,任务是串行执行的
- (void)sycnConcurrent{ dispatch_queue_t queue = dispatch_queue_create("com.ipa361.text1", DISPATCH_QUEUE_CONCURRENT); NSLog(@"---start-----"); dispatch_sync(queue, ^{ NSLog(@"sycnConcurrent 1 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"sycnConcurrent 2 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"sycnConcurrent 3 ---%@",[NSThread currentThread]); }); NSLog(@"---end-----"); /** ---start----- {number = 1,name = main}; {number = 1,name = main}; {number = 1,name = main}; ---end----- */}复制代码
- 同步函数+串发队列 - 不会开线程,任务是串行执行的
- (void)sycnSerial{ dispatch_queue_t queue = dispatch_queue_create("com.ipa361.text1", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ NSLog(@"sycnSerial 1 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"sycnSerial 2 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"sycnSerial 3 ---%@",[NSThread currentThread]); }); /** {number = 1,name = main}; {number = 1,name = main}; {number = 1,name = main}; */}复制代码
- 异步函数+主队列 - 不会开启线程,串行执行
- (void)asycncain{ //1. 创建队列 dispatch_queue_t queue = dispatch_get_main_queue(); //2. 封装任务交给队列 异步函数,我这堵着呢但是其他地方可以执行 dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 1 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 2 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"asycnConcurrent 3 ---%@",[NSThread currentThread]); }); /** {number = 1,name = main}; {number = 1,name = main}; {number = 1,name = main}; */}复制代码
-
同步函数+主队列 - 导致死锁
- 主队列把封装的任务交给主线程,而同步函数又是调用的主线程,这样互相等待了。
- 主队列的特点:如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务直到主线程空闲为止。
- 同步函数的特点就是马上执行,并且要等我的事情执行完毕之后才能执行其他的任务。
- 当queue把block从主队列拿出来想放到主线程中,发现主线程正在等待dispatch_sync执行完毕,这就导致互相等待导致死锁。
- 主队列把封装的任务交给主线程,而同步函数又是调用的主线程,这样互相等待了。
-
要是在子线程中执行syncMain消息的话,它不会造成死锁。因为是在子线程,主线程没有任务是空闲状态,所以才不会造成死锁。
- (void)syncMain{ //1. 创建队列 dispatch_queue_t queue = dispatch_get_main_queue(); //2. 封装任务交给队列 dispatch_sync(queue, ^{ NSLog(@"asycnConcurrent 1 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"asycnConcurrent 2 ---%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"asycnConcurrent 3 ---%@",[NSThread currentThread]); }); NSLog(@"_____end");}复制代码
6.3 GCD的线程通信
GCD实现线程之间通信手段靠嵌套。
- 从子线程回到主线程
//1. 创建子线程 // 第一个参数:队列 // 第二个参数: //因为是一张图下载所以,并发队列或者串行队列都可以,所以这里使用全局并发队列 //dispatch_get_global_queue 第一个参数是优先级 第二个参数留给未来使用 总是传0 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL *url = [NSURL URLWithString:@"http://img3.duitang.com/uploads/blog/201402/05/20140205123502_WyAa4.thumb.700_0.jpeg"]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; //更新UI,在子线程中 async和sync都是可以的,只有在主线程中同步和主队列放在一起用回造成死锁 dispatch_async(dispatch_get_main_queue(), ^{ self.imageV.image = image; }); });}复制代码
6.4 GCD的常用函数
6.4.1 延迟执行
- 调用NSObject方法的
performSelector:@selector(run) withObject:nil afterDelay:2.0
- 调用NSTimer的
scheduledTimerWithTimeInterval
- GCD的
dispatch_after
//延迟执行- (void)delay{ //1. 第一种 [self performSelector:@selector(task) withObject:nil afterDelay:2.0]; //2. 第二种 //Interval: 时间; repeats:是否重复调用 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES]; //GCD /* DISPATCH_TIME_NOW - 从现在开始计算时间 delayInSeconds - 要延迟的时间 (GCD的事件单位为纳秒,所以要*NSEC_PER_SEC(10的9次方)) dispatch_get_main_queue - 队列 GCD的延迟执行的优点是能控制延迟执行之后block能在哪个线程中执行;dispatch_get_main_queue是在主线程中,dispatch_get_global_queue是在子线程中 */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //代码块 NSLog(@"%@",[NSThread currentThread]); });}复制代码
说明
- NSObject方法在哪个线程调用,那么task就在哪个线程执行(当前线程),通常是主线程。
- (void)viewDidLoad{ [super viewDidLoad]; NSLog(@"打印线程----%@",[NSThread currentThread]); //延迟执行 //第一种方法:延迟3秒钟调用run函数 [self performSelector:@selector(run) withObject:nil afterDelay:2.0]; }-(void)run{ NSLog(@"延迟执行----%@",[NSThread currentThread]);}-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ //在异步函数中执行 dispatch_queue_t queue = dispatch_queue_create("wendingding", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ [self performSelector:@selector(test) withObject:nil afterDelay:1.0]; }); NSLog(@"异步函数");}-(void)test{ NSLog(@"异步函数中延迟执行----%@",[NSThread currentThread]);}复制代码
6.4.2 一次性代码
需求:点击控制器只有第一次点击的时候才打印,或者是单例。
#import "YYViewController.h"@interface YYViewController ()@property(nonatomic,assign) BOOL log;@end@implementation YYViewController-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ if (_log==NO) { NSLog(@"该行代码只执行一次"); _log=YES; }}@end//缺点:这是一个对象方法,如果又创建一个新的控制器,那么打印代码又会执行,因为每个新创建的控制器都有自己的布尔类型,且新创建的默认为NO,因此不能保证改行代码在整个程序中只打印一次。复制代码
- 使用
dispatch_once
函数能保证某段代码在程序运行过程中只被执行1次.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)复制代码
});
整个程序运行过程中,只会执行一次。
#import "YYViewController.h"@interface YYViewController ()@property(nonatomic,assign) BOOL log;@end@implementation YYViewController-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLog(@"该行代码只执行一次"); });}@end复制代码
注意:懒加载中不要使用dispatch_once函数
6.4.3 栅栏函数
需求就是有的任务依赖于其他线程先执行,之后再执行。
- 栅栏函数 - 想barrier1、2任务执行完之后再执行barrier3
- 栅栏函数不能使用全局并发队列,要不然有问题,只能使用自己创建的并发队列
- 栅栏函数之前的异步函数无法控制
- (void)barrier{// dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_queue_t queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ for (int i = 0; i<100; i++) { NSLog(@"barrier 1 ---%@",[NSThread currentThread]); } }); dispatch_async(queue, ^{ for (int i = 0; i<100; i++) { NSLog(@"barrier 2 ---%@",[NSThread currentThread]); } }); dispatch_barrier_async(queue, ^{ NSLog(@"barrier 3 ---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ for (int i = 0; i<100; i++) { NSLog(@"barrier 3 ---%@",[NSThread currentThread]); } });}复制代码
6.4.4 快速迭代
快速迭代之所以会比for循环快,是因为快速迭代内部会开启子线程(主线程也会参与执行任务),而for是在主线程中执行的。
/* 第一个参数:循环次数 第二个参数:队列(并发队列,传入主队列的话会死锁) 第三个参数:索引 */ dispatch_apply(10,dispatch_get_global_queue(0,0),^(size_t index){ NSLog(@"%zd --- %@",index,[NSThread currentThread]); });复制代码
做一个文件剪切案例,在一个form文件夹中有三张图片,需求就是把这个三张图片截切到to文件夹中。
//剪切图片- (void)moveFile{ // 拿到文件路径 NSString *from = @"User/Deskop/from"; // 获得目标文件路径 NSString *to = @"User/Deskop/to"; // 获得目标路径下所有文件 NSArray *array = [[NSFileManager defaultManager] subpathsAtPath:from]; //遍历所有文件,之后执行剪切操作 NSInteger count = array.count; for (NSInteger i = 0; count; i++) { //4.1 拼接文件全路径 stringByAppendingPathComponent在拼接的时候回自动添加斜杠 NSString *fullPath = [from stringByAppendingPathComponent:array[i]]; NSString *toFullPath = [to stringByAppendingPathComponent:array[i]]; //4.2 执行剪切操作 /* 第一个参数:要剪切的文件在哪 第二个参数:文件应该被放在哪里 第三个参数:错误, 传地址 */ [[NSFileManager defaultManager]moveItemAtPath:fullPath toPath:toFullPath error:nil]; }}- (void)moveFileWithGCD{ // 拿到文件路径 NSString *from = @"User/Deskop/from"; // 获得目标文件路径 NSString *to = @"User/Deskop/to"; // 获得目标路径下所有文件 NSArray *array = [[NSFileManager defaultManager] subpathsAtPath:from]; //遍历所有文件,之后执行剪切操作 NSInteger count = array.count; dispatch_apply(count,dispatch_get_global_queue(0,0),^(size_t i){ //4.1 拼接文件全路径 stringByAppendingPathComponent在拼接的时候回自动添加斜杠 NSString *fullPath = [from stringByAppendingPathComponent:array[i]]; NSString *toFullPath = [to stringByAppendingPathComponent:array[i]]; [[NSFileManager defaultManager]moveItemAtPath:fullPath toPath:toFullPath error:nil]; });}复制代码
6.4.5 队列组
用途:监听并发队列中的任务执行情况,可以在任务执行完成之后做逻辑处理。
- 第一种
//1.创建一个队列组 dispatch_group_t group = dispatch_group_create(); // global_quque 全局并发队列 /* - 封装任务 - 把任务添加到队列中 - 监听任务的完成情况,并且通知group */ dispatch_group_async(group, global_quque, ^{ }; //拦截通知,当队列组中所有任务都执行完毕的时候回执行下面任务 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的异步操作都执行完毕后,回到主线程... });复制代码
- 第二种
dispatch_group_enter
和dispatch_group_leave
配对使用
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_group_t group = dispatch_group_create(); //在该方法后面的异步任务会被纳入到队列组中监听范围 dispatch_group_enter(group); dispatch_async(queue, ^{ //告诉队列住,任务执行完毕离开队列组 dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(queue, ^{ //告诉队列住,任务执行完毕离开队列组 dispatch_group_leave(group); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的异步操作都执行完毕后,回到主线程... }); //除了dispatch_group_notify这个函数,还有一个函数能达到同样效果 //DISPATCH_TIME_FOREVER 表示死等并且这个函数是阻塞 dispatch_group_wait(group, DISPATCH_TIME_FOREVER); NSLog(@"end----");复制代码
问题:
dispatch_group_notify
是阻塞的么?那么dispatch_group_wait
呢?dispatch_group_notify
内部是异步线程,所以不是阻塞的。像end字符串就是第一行输出。dispatch_group_wait
内部是要等其他异步线程执行完毕才执行,所以是阻塞的。end字符串是最后一行输出。
需求:从网络上下载两张图片,把两张图片合并成一张最终显示在view上。 第一种方法
#import "YYViewController.h"//宏定义全局并发队列#define global_quque dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)//宏定义主队列#define main_queue dispatch_get_main_queue()@interface YYViewController ()@property (weak, nonatomic) IBOutlet UIImageView *imageView1;@property (weak, nonatomic) IBOutlet UIImageView *imageView2;@property (weak, nonatomic) IBOutlet UIImageView *imageView3;@end@implementation YYViewController-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ dispatch_async(global_quque, ^{ //下载图片1 UIImage *image1= [self imageWithUrl:@"http://d.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=2b9a12172df5e0fefa1581533d095fcd/cefc1e178a82b9019115de3d738da9773912ef00.jpg"]; NSLog(@"图片1下载完成---%@",[NSThread currentThread]); //下载图片2 UIImage *image2= [self imageWithUrl:@"http://h.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=f47fd63ca41ea8d39e2f7c56f6635b2b/1e30e924b899a9018b8d3ab11f950a7b0308f5f9.jpg"]; NSLog(@"图片2下载完成---%@",[NSThread currentThread]); //回到主线程显示图片 dispatch_async(main_queue, ^{ NSLog(@"显示图片---%@",[NSThread currentThread]); self.imageView1.image=image1; self.imageView2.image=image2; //合并两张图片 // 开启图片上下文,告诉宽度和高度 UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0); //画图 [image1 drawInRect:CGRectMake(0, 0, 100, 100)]; [image2 drawInRect:CGRectMake(100, 0, 100, 100)];//根据图片上下文得到一张图片 self.imageView3.image=UIGraphicsGetImageFromCurrentImageContext(); //关闭上下文 UIGraphicsEndImageContext(); NSLog(@"图片合并完成---%@",[NSThread currentThread]); }); // });}//封装一个方法,传入一个url参数,返回一张网络上下载的图片-(UIImage *)imageWithUrl:(NSString *)urlStr{ NSURL *url=[NSURL URLWithString:urlStr]; NSData *data=[NSData dataWithContentsOfURL:url]; UIImage *image=[UIImage imageWithData:data]; return image;}@end复制代码
问题:这种方式的效率不高,需要等到图片1.图片2都下载完成后才行。
提示:使用队列组可以让图片1和图片2的下载任务同时进行,且当两个下载任务都完成的时候回到主线程进行显示。
使用队列组
- 创建一个组
- 开启一个任务下载图片1
- 开启一个任务下载图片2
- 同时执行下载图片1\下载图片2操作
- 等group中的所有任务都执行完毕, 再回到主线程执行其他操作
#import "YYViewController.h"//宏定义全局并发队列#define global_quque dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)//宏定义主队列#define main_queue dispatch_get_main_queue()-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ //1.创建一个队列组 dispatch_group_t group = dispatch_group_create(); //2.开启一个任务下载图片1 __block UIImage *image1=nil; dispatch_group_async(group, global_quque, ^{ image1= [self imageWithUrl:@"http://d.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=2b9a12172df5e0fefa1581533d095fcd/cefc1e178a82b9019115de3d738da9773912ef00.jpg"]; NSLog(@"图片1下载完成---%@",[NSThread currentThread]); }); //3.开启一个任务下载图片2 __block UIImage *image2=nil; dispatch_group_async(group, global_quque, ^{ image2= [self imageWithUrl:@"http://h.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=f47fd63ca41ea8d39e2f7c56f6635b2b/1e30e924b899a9018b8d3ab11f950a7b0308f5f9.jpg"]; NSLog(@"图片2下载完成---%@",[NSThread currentThread]); }); //同时执行下载图片1\\下载图片2操作 //4.等group中的所有任务都执行完毕, 再回到主线程执行其他操作 dispatch_group_notify(group,main_queue, ^{ NSLog(@"显示图片---%@",[NSThread currentThread]); self.imageView1.image=image1; self.imageView2.image=image2; //合并两张图片 //注意最后一个参数是浮点数(0.0),不要写成0。 UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0); [image1 drawInRect:CGRectMake(0, 0, 100, 100)]; [image2 drawInRect:CGRectMake(100, 0, 100, 100)]; self.imageView3.image=UIGraphicsGetImageFromCurrentImageContext(); //关闭上下文 UIGraphicsEndImageContext(); NSLog(@"图片合并完成---%@",[NSThread currentThread]); }); }-(void)download2image{ dispatch_async(global_quque, ^{ //下载图片1 UIImage *image1= [self imageWithUrl:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"]; NSLog(@"图片1下载完成---%@",[NSThread currentThread]); //下载图片2 UIImage *image2= [self imageWithUrl:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"]; NSLog(@"图片2下载完成---%@",[NSThread currentThread]); //回到主线程显示图片 dispatch_async(main_queue, ^{ NSLog(@"显示图片---%@",[NSThread currentThread]); self.imageView1.image=image1; self.imageView2.image=image2; //合并两张图片 UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0); [image1 drawInRect:CGRectMake(0, 0, 100, 100)]; [image2 drawInRect:CGRectMake(0, 0, 100, 100)]; self.imageView3.image=UIGraphicsGetImageFromCurrentImageContext(); //关闭上下文 UIGraphicsEndImageContext(); NSLog(@"图片合并完成---%@",[NSThread currentThread]); }); // });}//封装一个方法,传入一个url参数,返回一张网络上下载的图片-(UIImage *)imageWithUrl:(NSString *)urlStr{ NSURL *url=[NSURL URLWithString:urlStr]; NSData *data=[NSData dataWithContentsOfURL:url]; UIImage *image=[UIImage imageWithData:data]; return image;}@end复制代码
- 补充说明
有这么1种需求:
首先:分别异步执行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(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...复制代码
});