多线程简介
进程:进程是描述一个程序的执行过程,是应用程序的一个实例(一个应用程序或软件可以有多个进程)。它是操作系统分配资源的基本单元,即拥有自己的地址空间、存储空间。
线程:线程是操作系统独立运行和独立调度的基本单位,可以理解为程序执行过程中的子过程,这个子过程可是并行也可串行(一个进程至少有一个线程)。
多线程:多线程是一种用来提高CPU运行效率的一种技术,往往提现为一个进程中分配了不止一条线程在执行任务,而是多条线程并发执行。
上面以自己的理解对进程
、线程
和多线程
概念的概括,需要详细了解相关知识的请自行Google、百度进行查阅。本文主要是以自己对多线程的理解进行梳理,如果有不对的地方请及时联系我更正。
生活中的多线程
其实多线程在我们的生活中有很多,如果说做饭是一个进程的话。我们把这个进程分成煲汤和煮面这两个线程(排骨汤配面条绝了😝),不会说把它们放到一个线程中依次进行,而是两个线程并发去完成,这样可以提高我们的执行效率。
上面的一个生活中的小例子只是概括的想象下程序的多线程是什么样子的,下面这个例子主要体现在多线程的处理效率上的提升:
上学的时候,同学们上完上午的课之后一起涌进食堂。如果食堂只有一个窗口,吃饭的人多了肯定会有很多人排队吃饭,这样的效率应该是最低的。食堂改革了新增了N个窗口提供用餐,我们可以将这种多窗口的改革称之为多线程的使用。这种方式能够解决执行效率低下的情况,排队时间缩短了M倍!除此之外,一个学校可能不止有一个食堂可能有第二、第三食堂,我们可以把这种情况看做是多核心处理。多核处理能让我们的执行效率得到更一步的提升!
上面叙述的目的就是让大家对多线程有一个直观的认识以及多线程处理性能高的优点。下面让我们了解下iOS开发中常用的多线程相关技术吧。
iOS多线程的分类以及详解
概述
iOS开发中提供了pthread、NSThread、GCD,NSOperation这四种方式来使用多线程。其中GCD
,NSOperation
使用频率比较高,将花费本博客的一大部分篇幅。而pthread
在普通日常开发中很少遇到,这里将仅仅介绍相关的简单使用方法。NSThread
在开发中用的频率也比较低,它是以面向对象的方式来处理多线程的。下面我们分别就这四种方式进行详细的介绍。
pthread
pthread由于开发中很少使用(基本没用过),所以这里只是对它做一个简单介绍,然后使用它调起多线程。
pthread的介绍参考百科如下:
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32。
使用pthread开启多线程:
pthread_create()有四个参数:
1.pthread_t *
:指向线程变量的指针
2.const pthread_attr_t *
:指向线程属性的常量指针
3.void * (*)(void *)
:指向返回值为void *
,入参为void *
的c函数(线程要执行的任务)
4.void *
:指向任务函数的入参指针
上面代码模拟了在开发中不影响主线程的情况下,开启子线程处理耗时操作(对应这里的打印10000次log)。其实这里还有很多问题,比如我们创建线程执行完任务后没有将该线程释放。我们可以在子线程任务结束的时候调用pthread_detach(pthread_self());
让线程运行结束后自动释放资源。
pthread这种方式使用c语言,以及需要程序猿自己管理线程的生命周期,大大降低了我们的开发速率。日常普通开发不推荐使用此方式,如果你想了解更多pthread相关知识请百度或谷歌寻找答案!
NSThread
NSThread
在开发中还是比较常见的,起码[NSThread currentThread]
大家都用过。相比于pthread的简单介绍,这里我们将通过NSThread基础用法
、线程的生命周期
、线程间的通讯
以及一个线程同步
对NSThread进行详细讲解。
NSThread基础用法
NSThread的实例化
NSThread
的实例对象作为一个线程的实例,我们所有的操作都是基于这个实例对象,那这样的实例对象该如何创建呢:
①使用类方法创建线程:
类方法创建的线程都没有返回对应的线程对象,如果要对该线程对象进行操作可以在对应的方法体中通过[NSThread currentThread]
获取当前线程。
②使用对象方法创建线程:
使用②中的对象方法创建的线程不会立即执行,需要对创建的线程对象调用start方法:[aThread start]
③使用NSObject的分类方法创建线程:
NSThread相关的实例化方法正如上面的三种分类所示,了解了创建线程实例对象后,我们要深入线程对象从对象相关的属性、用法来介绍。
NSThread常用属性&方法
该小节主要介绍官方提供的我们经常使用的那些类属性、类方法、对象属性、对象方法,来了解NSThread都有哪些功能。
常用的类属性&方法:
|
|
常用的对象属性&方法:
|
|
至此NSThread的实例创建以及基本功能大家都了解了,至少能使用NSThread开启一个线程做任务了。下面将对线程相关进行扩展,讲述线程的生命周期
是怎么在NSThread中体现的。
线程的生命周期
像很多的实例对象一样,线程也有自己的生命周期。线程的生命周期简单的来说就是线程的创建到线程的销毁的过程。本小节将围绕这个过程存在的状态:创建
、就绪
、运行
、阻塞
,销毁
进行讲解。
创建:创建一个线程对象,在调用start方法之前的状态。
就绪:就绪状态在代码中的常见形式是调用了start方法即:[aThread start]
,该状态描述的是线程对象已加入就绪队列中并等待CPU的资源。就绪状态也可描述为可运行状态,是运行状态的前一个必有的状态。该状态除了调用start方法还有很多其它情况,比如线程睡眠结束、共享资源锁的释放,IO操作完成等。
运行:当就绪的线程aThread得到CPU的资源时就会将线程对象激活进入运行状态,处理代码逻辑。
阻塞:阻塞是在线程运行中由于某种原因失去CPU资源的结果。常见的这些原因主要有:线程睡眠,IO操作,等待线程/锁。
销毁:销毁即线程的死亡状态,通常是运行完成后自动销毁,或者在运行期间调用了exit方法导致线程被结束。
线程的生命周期作为扩展小节,目的是让大家更好的理解线程是如何工作的,也为我们以后的开发能更好的处理问题提供线索。接下来讲述平常开发中常见的情景:线程间的通讯,来了解下各个线程如何沟通或者传递数据。
NSThread线程间的通讯
前面我们也说到了一个进程可以有多条线程在执行任务,这些线程之间的关系可能是相互独立的各自完成相应的任务,也有依赖关系的通常表现在一个线程所产生的数据,消息等通知给另一个线程。比如:用线程A去加载一个比较大的图片,加载完成后需要告诉主线程将图片渲染在屏幕上,我们把这种线程中的数据交流称为线程间的通讯。
NSThread线程通讯的常用方式:
在NSObject的NSThreadPerformAdditions
分类中提供了从一个线程调起另一个线程的方法,下面我们将围绕这几个方法进行介绍,最后以一个例子结束线程通讯模块。
|
|
|
|
|
|
下面代码主要演示主线程绘画一个UIImageView,而它的图片来自于互联网需要开启一个异步线程去下载,当图片下载好之后需要主线程将图片再渲染到屏幕上,这样来模拟线程间的通讯的。
|
|
这里注意下dataTaskWithRequest: completionHandler:
这个方法的block代码也是异步调用的,虽然不在我们指定的线程中通知主线程进行刷新UI,但是不影响我们进行线程间通信的演示。
NSThread线程同步
多线程环境下存在同时访问共享数据的情况,如果不对数据的访问进行处理/限制极有可能造成数据混乱或是数据处理异常。这里我们引用经典的买票案例演示多线程访问存在的问题,以及解决办法。
|
|
|
|
上面代码演示了北京和上海两地同时卖票的情况,结果打印的数据存在着问题,而造成这种问题的原因是线程间共享数据的读和存不同步。比如上海读取票数为10,在上海修改票数之前北京读取也为10,这样两个窗口卖了两张票之后总票数还为9。通常处理这种问题的方案是给这个读存操作加锁,即在一个窗口A访问数据时其他窗口不能访问该数据直到A访问完成。
解决方案:对售票saleTickets
业务访问共享数据的逻辑加锁操作:
运行结果如下:
可以看出对共享数据访问加锁的方式成功解决了数据混乱的问题。在iOS开发中加锁的方式不只是使用@synchronized(){}
还有其他类型的锁这里不一一介绍,有兴趣的童鞋可以搜搜相关博客介绍。
到此NSThread的相关知识点从基本介绍到多线程数据的访问限制已经比较全面的讲述了一番,因为NSThread的轻型以及面向对象开发方式,总体来看使用NSThread操作多线程是还比较容易上手的。在其优点的对面当然也有它的不足:我们需要管理线程的生命周期,对共享数据加锁开销比较大。NSThread相关介绍到此结束,下面让我们看看开发中使用频率非常高的GCD
吧!
GCD
GCD是Grand Central Dispatch
的缩写,由苹果公司开发的优化多核处理器的技术。它的基本思想是将线程的管理从开发中分离出去,让开发者更专注于自己的业务开发。在日常开发中经常使用GCD处理多线程相关业务,所以学会使用GCD是非常重要的。按照NSThread的套路我们仍然从基础开始由浅入深的对GCD进行讲解,力求大家看完GCD章节内容后能熟练的使用它。本节将通过:GCD相关概念
、常用API
、GCD的基本使用
、线程间通信
,线程安全
对GCD进行详细讲解。
GCD相关概念
因为GCD执行的方式是将任务放入队列中进行同步/异步执行,所以在使用GCD之前,我们先对任务
和队列
这两个核心概念进行了解。
任务:简单的说我们要执行的代码片段要完成的业务就是一个任务,而开发中这些任务一般是处理一些耗时的操作。通常任务都被描述为被执行的,在GCD中执行任务分为两种形式:同步执行
,异步执行
。同步执行不具备开启新线程的能力,就在原线程中执行任务。而异步执行是具备开启新线程的能力的,但是开不开、开几条线程是跟队列相关的,后面我们会在GCD的基本使用
小节中详细介绍他们的用法。
队列:这里的队列是用来存放任务的,也称为任务队列。GCD中分为串行队列
、并行队列
两类。串行队列其特点是一个任务执行完了才执行下一个任务遵循队列的先进先出原则,而并行队列在异步执行的情况下可将各个任务依照先进先出原则并发执行。
概念补充:
并发执行:并发描述为多个事件在同一时间间隔内发生。由于CPU的处理速度非常快,CPU完全有能力在短暂的时间间隔内完成多条线程的切换以及各个线程部分任务的执行。并发能够有效提升CPU的执行效率。
并行执行:并行描述为多个事件在同一时间点发生。并行执行发生在拥有多核处理器的设备上,在同一时间点不同的处理器执行不同的线程不同的任务。并行的多核处理器仿佛拥有多个心脏/引擎一样拥有更强大的’动力’,结合并发执行能够显著提升任务的执行率。
GCD基础
在了解了基本的概念之后我们主要通过队列
、同步异步执行
来了解GCD的使用。
dispatch_queue队列
队列在GCD中的类型为dispatch_queue_t
,本小节将围绕队列的创建、使用和特性进行讲解。队列除了按照性质分类的串行和并行队列,我们还可以将队列分为主队列
、全局队列
和自定义队列
下面让我们以这三种分类对队列进行介绍。
主队列:即主线程队列,在我们的开发中主线程即UI线程。主队列为串行队列,在开发中无法自定义创建,其获取方式可以使用官方提供的APIdispatch_get_main_queue()
获取如下:
全局队列:是系统提供方便编程的一个并行队列,其获取方式使用dispatch_get_global_queue(long identifier, unsigned long flags)
。其中有两个参数identifier
,flags
分别表示队列的优先级以及预留参数。
identifier优先级分了四个等级,在NSThread章节讲过创建的线程我们也可以给它设置优先级,那么这两块的优先级有什么联系呢,下面针对GCD全局队列的四个等级进行映射:
对应的优先级关系可参考NSThread线程优先级介绍,由于全局队列处理一些耗时/后台的一些操作,其优先级并没有提供与NSQualityOfServiceUserInteractive对应的刷新UI的优先级。
flags预留参数:该参数作为预留参数现阶段开发中并没有什么用,但是官方提到Passing any value other than zero may result in a NULL return value.
大概意思就是如果此处传参不为0可能返回一个NULL的队列,所以开发中这个参数我们都写0即可。
|
|
根据打印的log我们发现了一个问题,就是相同优先级获取到的队列是同一个队列,而不同优先级获取的队列地址是不同的。也就是说全局队列不止一个队列对象是四种不同优先级的四个全局队列组成的。
自定义队列:除了使用系统提供的主队列和全局队列,我们还可以根据自己的需求创建队列,自定义队列也是分串行队列和并行队列的,下面让我们看看自定义队列如何创建:
dispatch_queue_create(const char *label,dispatch_queue_attr_t attr)
方法提供了两个入参:label为线程唯一标示方便于debug找问题,attr是队列属性用于控制创建的队列是串行(DISPATCH_QUEUE_SERIAL)还是并行(DISPATCH_QUEUE_CONCURRENT)的。
sync同步&async异步执行
到现在我们已经知道如何创建不同类型的队列,那么我们如何执行队列中的任务呢。GCD中提供了同步执行和异步执行任务,同步和异步执行的特点在GCD概念介绍的时候已经讲过这里不再赘述。下面让我看看两种方式的使用吧:
同步执行:dispatch_sync(dispatch_queue_t queue, ^(void)block)
同步执行方法接收两个参数,queue指定要在哪个任务队列中执行,block所指代码块为要执行的任务。
异步执行:dispatch_async(dispatch_queue_t queue, ^(void)block)
和同步执行方法类似也拥有相同的入参,请参考同步执行入参介绍。
下面将结合串行并行队列诠释同步与异步执行的不同:
同步+串行队列:不会开启新线程,任务遵循先进先出方式依次执行。
前面讲队列的时候我们说了主队列也是属于串行队列的,如果同步执行相同队列中的任务会是什么样的情况呢,下面展示主线程中在主队列中执行任务:
运行的结果是程序crash了EXC_BAD_INSTRUCTION(code=EXC_I386_INVOP,subcode=0x0)
,因为同步串行执行需等前一个任务有返回值或者说结束了才会去执行下一个任务,示例中添加的task1在串行队列的最后面,前一个任务没有执行完要接着执行,而此时又要立即执行task1,出现了任务互等相互死锁都执行不了。
主队列是一个比较特殊的串行队列,那是不是一般的串行队列也有这样的问题呢:
这里发生了相同的crash,也就是说同步执行相同串行队列的任务时会发生死锁,所以在多线程开发中不要走这种雷区。不过这种情况一般人也不会这样写,直接插入对应的任务代码即可何必再用GCD同步执行该任务呢!文章中提这种问题就是一种对GCD用法的研究,小伙伴们不要太在意。
同步+并行队列:不会开启新的线程,虽然队列是并行队列但是同步执行不具备开启新线程的条件,所以只能在该线程中依次执行。
|
|
如上同步并行执行,由于没有开启新的线程,task1-6在主线程中顺序执行,由嵌套同步并行执行代码可看出放入到并行队列的任务会立即执行,直到执行完成才接着做原来任务继续执行(同步执行下)。
异步+串行队列:可以开启一条线程,任务在串行队列中依次被执行。
异步串行执行情况下,最多可创建一条新的线程(提供一个新的串行队列情况下),当串行队列相同时不会创建新的线程如嵌套异步串行执行所演示。在同一个串行队列的任务依次执行,也可以从task5和其子任务可看出,task5比5.1,5.2,5.3先加入队列中,需要等task5任务结束之后再去执行其后的子任务。如果将注释代码打开serialQueue = dispatch_queue_create("anotherSerialQueue", DISPATCH_QUEUE_SERIAL)
就可演示非同一个串行队列的任务不必等到当前任务执行完成才能执行,当然这属于异步的范畴本就该如此。打开注释打印log如下:
异步+并行队列:会开启一条或多条线程(依赖加入到并行队列的任务数),但是不会无限制的开启新的线程,毕竟开启线程、管理线程也会浪费很多CPU资源。异步并行演示如下:
|
|
异步并行执行给我们的感觉就是你并不知道它什么时间会被调用,但总体上来看是各个线程交替执行符合并发的特点。这种执行方式可以大大的提高业务的执行效率提高CPU的性能,所以在开发中遇到多种耗时操作时使用异步并行处理。
以上是对异步、同步配合串行、并行队列的四种情况的详细介绍,演示囊括了开发中所遇到的所有基本情况。建议尽量理解这四种的用法!
如果你看到这里说明你已经能够运用GCD处理多线程了,除了那些基本的用法之外GCD还有很多其它的用法。以下的几个小模块都将围绕GCD扩展功能来叙述!
dispatch_after
GCD提供了延迟加入任务的方法,满足延迟执行的需求:
下面演示延迟执行代码:
这种显示创建dispatch_time_t的可以被类似下面的这个方法替代:
dispatch_once
dispatch_once提供了代码只被运行一次的功能,这种功能一般使用场景如:单例的创建,程序基础资源初始化,下面让我们来看看如何使用它吧!
GCD单例演示:
dispatch_source
dispatch_source是一种用来处理操作系统底层级别事件的数据类型,它能够处理很多类型的事件这里我们只研究下Timer dispatch sources
定时器类型。
|
|
|
|
其实在开发中我们最常用的定时器应该是NSTimer,NSTimer在runloop任务比较多的时候精度可能跟不上我们的需求,这时就可以用Timer dispatch sources来更精确的处理我们的定时任务了。dispatch_source还可以处理很多的事件,有兴趣的童鞋可以自己去网上查查相关资料。
dispatch_group
dispatch_group提供了任务组的概念,在组内的任务串行或并行全部完成之后再做统一处理。在日常开发中有时可能遇到这样的情况:在一个页面里面需要的数据一个接口给的不全,还需要调用另一个接口。两个接口返回的结果整合起来去刷新UI,因为这两个接口异步调用返回时间不确定普通处理方法比较麻烦。使用队列组处理这类问题就变得简单了。下面演示队列组的使用:
场景一:任务A先执行,A执行完成后任务B使用A所产生的数据做进一步数据处理,两个任务完成后回主线程再进行处理。
场景二:任务A和任务B各自执行(异步并发),执行后到主线程统一处理数据。
场景三:以上两个场景的混合使用,任务A和任务B异步并发执行,而任务A又分了两个子任务同步执行,最后将任务A和任务B的数据整合到主线程处理逻辑。
dispatch_apply
dispatch_apply其实可以看作是同步执行dispatch_sync与任务组dispatch_group的组合体,适用于重复的任务体添加。既然是任务组又是同步执行,所以在dispatch_apply之后的业务都会等待它里面的任务都执行完了才会执行。下面对dispatch_apply的几种使用情况进行分析:
同步+dispatch_apply串行队列:不会开启新的线程就在原线程执行任务,第一个任务执行完后才会执行下一个任务。
同步+dispatch_apply并行队列:可以开启新线程,根据任务和系统资源决定,任务可能在本线程中(main)执行,多个任务并发交替执行。
异步+dispatch_apply串行队列:不会开启新线程,就在异步线程中任务依次执行,第一个任务执行完才会执行第二个任务。其实这种情况类似于同步+dispatch_apply串行队列,只不过换了个线程去执行任务罢了!
异步+dispatch_apply并行队列:可以开启新线程,开线程数量跟任务和系统资源相关,除了开启新的线程,任务可在原异步线程中执行,多个任务并发交替执行。
关于dispatch_apply相关介绍如上,需要我们注意的是在调起dispatch_apply的线程会被阻塞,需等待dispatch_apply内所有任务执行完成后再接着执行下面的代码。
dispatch_barrier
GCD的栅栏主要用来对多个任务进行分割,即先完成前面一部分然后在完成后面一部分,下面让我们看看相关使用情况。
场景:任务A,B,C,D都是进行异步执行,但是有个需求是先让A,B完成后再去执行C,D。听起来跟任务组类似,任务组是可以完成这类操作的,但是使用栅栏显得更容易点。
注意:The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_sync function.
官方给的说明大概意思是栅栏效果需使用自定义的并发队列才有效果,使用串行队列或者全局并发队列只是一个同步执行任务一样。使用dispatch_barrier_async和dispatch_barrier_sync区别在于是否开启新的线程执行barrier里面添加的任务。
线程间通信
线程间的通讯在NSThread章节已经叙述过大概的使用场景,这里我们就不再赘述。按照之前的例子我们使用GCD来实现一下:
|
|
线程安全
GCD的线程安全延续NSThread所使用的卖票演练例子:
NSOperation
NSOperation是基于GCD进行封装的具有面向对象特点的多线程管理方案,使用它不需要关心线程生命周期,任务处理变得更方便。如果GCD章节看过了那么使用NSOperation管理多线程也将变得清晰明了,除此之外而NSOperation为抽象类,在实际开发过程中可以使用它的两个子类或者自定义子类,这些operation映射到GCD中的任务,除了任务开启多线程还需要任务队列NSOperationQueue来进行多线程管理。在这一章节中我们将围绕NSOperation中的基础API
、任务&队列
、线程通讯
、其它特性
进行详细讲解。
基础API
该小节是对相关的类以及其属性和方法进行介绍主要包括以下类:NSOperation
、NSBlockOperation
、NSInvocationOperation
、NSOperationQueue
通过上面几个类的介绍给大家展示基础功能,对使用NSOperation多线程有初步的了解。
NSOperation:
NSBlockOperation:
NSInvocationOperation:
NSOperationQueue:
任务&队列
在了解相关基础后我们开始着手使用NSOperation开起多线程,由于NSOperation是基于GCD进行封装的,所以NSOperation与GCD有很多相似之处,比如任务和队列的概念。只不过这里的任务和队列其表现形式有一定的变化,下面针对任务和队列进行详细介绍。
任务
任务在NSOperation中的表现为任务对象,上面也提到过了通常我们使用的任务类是NSOperation
的子类NSBlockOperation
、NSInvocationOperation
以及自定义子类。那下面我们就按照这个顺序熟悉下任务的使用。
NSBlockOperation:
上面代码演示一个最简单的任务执行,跟GCD的执行任务方式不一样需要我们手动调用start
方法才开始执行任务,由打印日志看出我们的任务就在本线程中同步执行的。除此之外NSBlockOperation
还提供了另一个方法用来追加任务addExecutionBlock:
下面我们看看它的特点。
|
|
使用addExecutionBlock:
方法添加的任务块能够在其它线程进行并发执行,这个也是使用NSBlockOperation
的一个特点。还有个需要我们注意的,虽然task2是异步执行但是根据打印的日志可以看到task end!
在task2执行完后才执行,所以这种方式的异步执行会阻塞当前线程。
NSInvocationOperation:
如上演示NSInvocationOperation
的使用,同样是同步执行没有开启新的线程。
自定义子类:
|
|
上面自定义任务模拟了类似NSInvocationOperation
的调用方式,自定义任务主要是重写main
方法,从打印的log我们可以看出是在start
方法调用main
方法的,所以要自定义异步并发方式的话可以重写start
方法以及控制一些标示位,参考如下官方文档说明:In a concurrent operation, your start() method is responsible for starting the operation in an asynchronous manner. Whether you spawn a thread or call an asynchronous function, you do it from this method. Upon starting the operation, your start() method should also update the execution state of the operation as reported by the isExecuting property. You do this by sending out KVO notifications for the isExecuting key path, which lets interested clients know that the operation is now running. Your isExecuting property must also provide the status in a thread-safe manner.
Upon completion or cancellation of its task, your concurrent operation object must generate KVO notifications for both the isExecuting and isFinished key paths to mark the final change of state for your operation. (In the case of cancellation, it is still important to update the isFinished key path, even if the operation did not completely finish its task. Queued operations must report that they are finished before they can be removed from a queue.) In addition to generating KVO notifications, your overrides of the isExecuting and isFinished properties should also continue to report accurate values based on the state of your operation.
队列
上面的小节我们演示了任务是如何单独使用的,我们要知道没有NSOperationQueue
的配合是不会开启异步线程的(addExecutionBlock这个例外)。既然我们就是使用NSOperation来实现多线程的那我们这一小节就来了解下NSOperationQueue
相关以及配合几种任务的使用吧。
队列简介:NSOperationQueue提供了两种队列一个是主队列
、自定义队列
。在主队列中的任务都是同步执行的,而自定义队列中的任务根据队列属性设置可表现为同步或者异步执行。
|
|
|
|
添加任务:
有了队列我们就可以给队列添加任务了,
|
|
如上log,我们看出了自定义队列情况下开启了新的线程,而新的线程的调度情况依赖maxConcurrentOperationCount
的数值而定。使用默认的数值即不设定数值,开启新线程数量以及并发数都依赖系统可用资源。使用0时队列中的任务不会分配资源,使用1时队列中的任务依次执行,使用n(n>1)时队列的任务最高可并行执行数量为n,任务异步并发执行。
如下所示n=2的log:
下面看下在mainqueue中的测试:
没有开启新的线程就在主线程中队列中的任务依次执行!NSOperationQueue
的添加任务除了addOperation:
还有addOperationWithBlock:
、addOperations:waitUntilFinished:
这些方法就不一一测试了有兴趣的童鞋可以自己用用。
其它特性
到此NSOperation的相关基础用法已经介绍完了,下面这些特性也是要熟练使用的,因为这些特性能够让NSOperation的操作变得更加的灵活,本节主要讲述任务依赖
、任务完成回调
、队列取消暂停恢复
。
任务依赖
任务依赖感觉一点任务组的意思,因为在之前的GCD中我们处理开发中多个接口的情况就是用了GCD的dispatch_group,而这个任务依赖也是处理这种情况的下面让我们看看是如何使用的:
任务依赖如上所示,这个演示了我们日常开发中很常见的一种场景就是一个页面的数据需要两个或者多个接口提供,通常处理是让两个接口同时去请求可以减少用户等待时间提高用户体验。等到两个接口的数据都回来的时候我们统一在主线程处理去进行UI刷新。根据log打印任务是按照我们的设计来执行的,任务依赖就到这吧。
任务完成回调/监听
在我们介绍基础APINSOperation
任务中有一个属性@property (nullable, copy) void (^completionBlock)(void)
完成回调,我们可以设置这个block对完成任务进行监听。通过这个完成时的监听可以统计一些信息或者对任务进行进一步的操作,下面让我们看看它的使用吧。
任务取消暂停恢复
取消:
注意:任务取消除了队列的cancelAllOperations
方法,其本身也有一个取消的方法cancel
。这里演示队列层面上的操作,根据日志我们可以得知分配到线程的任务取消方法是没有用的。
暂停&恢复:
|
|
其实队列的暂停和队列的取消是有一样的地方的,比如分配到线程的任务是无法暂停的,暂停也是针对那些在队列中没有执行的任务。而暂停相比于取消有一个特点就是暂停掉的任务是可以重新开启恢复的,利用这个特点可以在很多的开放场景中运用的到。
多线程演练
使用NSOperation进行多线程的讲解基本到此为止,我们最后将经典的卖票列子使用NSOperation进行演示。这里不适用同步锁或者GCD中的相关方法,利用NSOperation相关的多线程同步来解决数据同步问题:
|
|
小结
本文经过了数周的间断性更新,终于将iOS多线程pthread
、NSThread
、GCD
、NSOperation
的使用方法、使用步骤以及注意点都给总结梳理了一番。如果你是一点一点跟着文章中的演示代码来的我觉得你应该已经能够熟练的使用iOS中这几种多线程了,更深入的研究还有待小伙伴不断的努力。下面我们在对这几种方法的优缺点简单的描述来结束该篇博客:
pthread:使用极少
-优:纯C语言实现效率高,跨平台
-缺:代码可读性差,需要开发管理线程生命周期
NSThread:使用比较少
-优:轻量级的管理多线程方式
-缺:需要开发手动管理线程的生命周期,线程同步系统开销较多
GCD:使用很多
-优:处理多线程效率高,能够适用多核CPU处理,不需要管理线程的生命周期让开发把更多精力放到业务上
-缺:C语言API代码可读性不强
NSOperation:使用很多
-优:底层封装GCD 具有面向对象特性,不需要管理线程,将执行的操作抽象为任务对象操作更加的方便,能够灵活控控制任务的进行方向。
以上是对iOS多线程的总结,由于是自己的理解上写的这篇文章,如果你发现本文出现了错误的地方还请及时联系我及时更正!