前言
本文参考自《iOS开发项目化经典教程》第一章,主要讲述iOS实现多线程的四种方式,线程间的安全和通信,GCD的基本操作。NSOperation的基本操作。
多线程概念:由于一个线程同一时间内只能处理一个任务,因此一个线程内的任务需要按顺序执行。因此在遇到网络请求,下载等耗时操作时,需要等待此类操作结束才能进行接下来的操作,这段时间内用户不能进行任何操作,app也不会响应用户的操作。这是一种很糟糕的用户体验。因此在开发过程中,通常把比较耗时的操作放到一个线程中,把和用户交互放到主线程中。保证能够及时响应用户的操作行为。
进程和线程
进程:进程是系统中正在运行的程序,进程拥有自己的地址空间,程序加载进内存开始运行时就会变为一个进程。进程是系统进行资源分配和调度的基本单位。有三个特点:1、独立性,进程有独立的资源和地址访问空间。2、动态性,进程是程序在系统中的一次执行过程,进程时一个正在系统中活动的指令的集合,其中加入了时间的概念。具有自己的生命周期和各自不同的状态。3、并发性,进程可以在单个处理器中并发执行,虽然宏观上是并发的,但是从微观上看,cpu在一个时间片内只能执行一条指令,因此在宏观时间内也是各个进程不断相互切换执行。
线程:线程是任务调度和执行的基本单位,线程扩展了进程的概念,一个进程要运行至少需要一个线程,这个线程成为称为主线程。当进程被初始化后,线程就被创建了。进程内的每个线程是相互独立的。
进程中包含若干线程,这些线程可以利用进程所拥有的资源。由于线程比进程更加轻量,对线程的调用开销就会比进程的开销小的多。因此多线程更能提高系统中多个程序间的并发程度。当使用多线程进行开发时需要注意:1、线程虽然占用资源较低,但是也需要一定的内存空间,如果开启大量线程,就会占用大量内存空间导致程序卡顿。3、开启的线程越多,cpu在进行调度时的开销就越大。因此不要同时开启超过5个线程。3、使用多线程时需要保证数据的统一性。可以采用加锁的方式实现数据的独占访问。
线程的串行和并行
一个进程中如果只包含一个线程,那么当多个任务需要执行时只能由这个线程去一个一个串行执行。如果进程中包含不止一条线程,那每条线程可以同时执行不同的任务,称之为线程的并行,也即多线程。需要注意的是,cpu在同一个时间内只能处理一个线程,也就是同一时间只有一个线程在工作,不过cpu切换很快,用户感知不到。
多线程分类
iOS提供了四种现实多线程的方式,pthread:使用c语言实现,是跨平台的多线程api。NSThread:使用oc实现。GCD:使用c实现,对线程管理进行了封装,是常用的多线程开发手段。NSOperation:基于GCD进行了二次封装,使其使用更加面向对象。
线程的状态
当线程被创建并启动时,线程并不是直接开始执行,而线程开始执行之后也不是一直在执行。而是随着cpu的时间片轮转不停的切换状态。下图是线程的状态切换过程图。
1、创建,当创建一个线程之后,该线程就处于新建的状态,仅由系统分配了内存,初始化了其内部成员变量的值。2、就绪,当线程调用了start方法之后,线程就处于就绪态了,处于就绪态的线程并没有开始执行,只是表示可以开始执行了。何时开始执行由系统控制。 3、运行,当cpu分配的时间片调用当前线程时,其他线程处于挂起状态,该线程处于运行状态。4、终止,当线程执行结束,或者调用exit方式终止,或者执行过程中出现异常,线程变为终止状态。5、阻塞,如果当前线程需要暂停一段时间,则可以调用sleep方法让线程进入阻塞态。
线程间的安全隐患
如果进程中的一块资源被多个线程共享,那么这块资源称为临界资源,当多个线程同时访问一块资源时,会出现资源的抢占和数据错乱等问题。为了实现数据的安全访问,可以使用线程间加锁的方式,下面通过一个卖票的例子展示一下:
从途中可以看到,多个线程访问临界资源时会发生数据错乱,为了解决这种问题,需要给临界资源加锁,加锁可以保证同一时刻只有一个线程能够访问临界资源,使用@synchronized关键字可以实现加锁.需要注意的是使用同步锁会消耗大量cpu资源,所以应该把同步锁内的代码量尽量减少.
线程间通信
在一个进程中,其线程往往不是孤立存在的,他们之间可以相互通信,比如一个线程需要等到另外一个线程任务执行完毕后才能开始执行.在做任务转换的同时也可能有数据的转换.
使用NSThread实现多线程
下面通过一个实例来演示NSThread的三种创建方式。
1 | /// 需要调用start启动线程 |
三种不同的线程创建方式:
1 | - (void)viewDidLoad { |
主线程的名字是main,手动创建的线程可以重新命名。
使用GCD实现多线程
使用GCD可以比NSThread更快,更方便的实现多线程的处理,使用GCD需要明确两个概念,队列和任务.队列:队列是GCD中用来存放任务的集合,负责管理开发者提交的任务.队列的核心就是将长期执行的任务分成多个工作单元,并将任务添加到队列中,系统会带为管理这些队列.并放到多线程中执行.队列分为串行队列和并行队列.串行队列内部只维护一个线程,一次只能执行一个任务.任务串行执行.并行队列内部维护了多个线程,可以按照如队列顺序并行执行. 任务:任务时用户提交给队列的工作单元,也就是在队列中执行的代码块.任务提交后会由队列以多线程的方式执行.
创建队列
全局并发队列,全局并发队列可以并行的执行多个任务,
1 | //identifier 指定队列优先级 |
创建串行队列和并行队列
1 | // 创建队列 |
获取主队列
1 | /// 获取主队列 |
提交任务
队列创建结束之后,可以通过同步和异步两种方式向队列提交任务. 同步执行:同步执行的任务只会在当前线程执行任务,不会开启新线程.
1 | ///同步执行 |
异步执行任务,异步执行会在新线程中执行任务具备开启新线程的能力,
1 | ///异步执行 |
下面是一个代码实例,展示了串行队列下的同步执行和并行队列下的同步执行,串行队列下的异步执行和并行队列下的异步执行.
同步执行不会开启新的线程,是在主线程完成任务的,同步的并行也不会开启新线程,两个都是串行执行任务.
异步执行串行队列的任务,会开启新线程,但是任务串行执行.
异步执行并行队列任务,会开启新线程,任务并行执行.
下表展示了同步执行串行队列任务和并行队列任务,异步执行串行队列任务和并行队列任务的结果
单次或者重复执行任务
如果想要某些操作只执行一次,可以使用dispatch_once()实现,干函数接收两个参数,第一个参数接收一个标识符,用来标识代码代码是否已经执行.第二个参数是只执行一次的代码块.如果想要某些操作重复执行多次,可以使用dispatch_apply();
1 | /// iterations 需要执行的次数 queue 任务需要提交的目标队列 block 需要重复执行的代码块 |
调度队列组
假设有如下一个一个场景,一个音乐app,需要执行多个下载任务,这些下载任务会放到后台的多个线程中执行,当全部下载任务结束之后,弹出一个提示框告知用于下载结束.
这种场景可以使用队列组在实现.使用dispatch_group_create()创建队列组,
1 | /// 创建队列组 |
创建队列组后,可以使用dispatch_group_async()函数将需要执行的任务提交到队列组,会异步的执行任务组中的任务,
1 | /// 异步执行队列组中的任务 group,队列组 queue,任务需要提交到的队列, block,需要执行的代码块 |
任务组中的任务执行完之后可以通过dispatch_group_notify()来通知,
1 | /// group, 创建的队列组 queue,将要执行的任务所添加到的队列 block,执行的代码块 |
这里的demo展示的是使用队列组进行图片下载,在图片下载完成后,回到主队列进行渲染和展示.但是队列组中的每一次下载都是新开了线程,并且是并行执行的.
1 | - (void)groupImage { |
使用NSOperation实现多线程
NSOperation是使用oc封装的基于gcd的多线程处理方式,更加面向对象1、执行操作,如果要执行一个NSOperation对象,可以通过1、手动调用start方法实现,这个方法调用之后,就会在当前调用的线程同步执行任务.2、可以将NSOperation添加到NSOperationQueue中,NSOperationQueue会在NSOperation被添加进去后尽快执行,并且是异步执行.2、取消操作,当一个operation开始执行后,默认会一直执行到结束,也可以调用cancel取消操作的执行,需要注意的是,如果操作在队列中没有开始执行,这时取消这个操作,并将finished设置为YES,此时操作就直接取消了.如果操作正在执行,设置cancel方法也只能等待操作执行完.3、添加依赖,可以将多个耗时的异步操作分成若干部分,当前一个执行完后在执行另一个,可以通过addDependency方法,协调先后关系.注意:两个任务间不能相互依赖,比如A依赖B,B依赖A,这样会导致思索.当每个操作结束后需要将isFinished设置为Yes.4、监听操作,使用setCompletionBlock可以在一个operation结束后做一些其他事情.
NSOperationQueue
NSOperationQueue与GCD队列一样,采用先进先出的方式,负责管理系统提交的多个NSOperation对象.NSOperationQueue负责管理其中持有的NSOperation对象,下面是一些常见的方法:
1、添加NSOperation到NSOperationQueue中,将NSOperation添加到队列中,并由队列中的线程池管理和调度,使用如下方法来管理,
1 | /// ops,将要添加到队列的操作组, wait 是否等待操作结束后在返回,设置为yes会阻塞主线程 |
也可以使用如下方法来添加任务,添加任务到队列中后不要在手动更改任务的状态,队列会自行进行管理.
1 | /// block 将要执行的代码块 |
2、修改NSOperation对象执行顺序,对于添加到队列中的操作对象,执行顺序依赖亮点,1、操作对象是否已经是就绪态,2、操作对象的优先级高低.可以通过手动设置queuePriority在更改operation的优先级.
3、设置操作队列的最大并发数量,当队列中线程过多时,会影响app的执行效率,因此可以设置对答并发数量来约束队列中的最大进程数
4、等待NSOperation操作执行完成,不要在主线程使用如下方法,这回阻塞主线程.app显示为无响应.
5、使用NSOperation子类进行操作,NSOperation是一个抽象基类,可以自行继承这个类创建自己的操作对象,也可以使用NSInvocationOperation和NSBlockOperatin来进行操作.
1 | - (void)waitUntilFinished ; |
因此,使用NSOperation和NSOperationQueue两个类结合使用可以实现多线程,1、将需要执行的操作添加到NSOperation中.2、将NSOperation添加到NSOperationQueue中.3、系统自动取出NSOperationQueue中的对象.4、取出的对象会自动创建新线程进行.
iOS多线程小demo
1 | dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); |
上述代码执行结果为13245或者12345,结果是固定的.明确概念,任务是执行在队列上的,队列是运行在线程上的.一个线程上可以同时出现多个队列.同步任务会阻塞线程.异步任务不会阻塞线程.异步任务具有开启新线程的能力.如果是自定义队列,异步执行会为其开启新线程,如果是主队列,异步执行不会开启新线程,在主线程上执行.
对于上面代码,首先打印1,然后异步执行串行队列上的任务,不会阻塞当前线程(主线程),打印3,4和5的顺序是固定的,因为4是同步任务,2和4的顺序是固定的,因为两者在同一个队列上,需要按照fifo的顺序执行.因此,只需要考虑2和3谁先谁后即可,应该说都有可能,因此最后结果是13245或者12345.
上文中的demo参考此处即可.