ios多线程详解

                                            ios多线程详解

一、前言

在ios中每个进程启动后都会建立一个主线程(UI进程),这个线程是其他线程的父线程。由于在ios中除了主线程,其他子线程都是独立于Cocoa Touch的。多线程的实现有以下几种方式:

NSThread:

(1)使用NSThread对象建立一个线程,非常方便。

(2)但是!使用NSThread管理多个线程非常困难,不推荐使用。

GCD--Grand Central Dispatch:

(1)基于C语言的底层API。

(2)用block定义任务,使用起来非常灵活方便。

(3)提供了更多的控制能力以及操作队列中所不能使用的底层函数。

NSOperation/NSOperationQueue:

(1)是使用GCD实现的一套Object-C的API。

(2)是面向对象的线程技术。

(3)提供了一些在GCD中不易实现的特性,如:限制最大并发数量、操作之间的依赖关系。

二、线程与进程

1.进程

进程是系统进行资源分配和调度的基本单位,每一个进程都有自己独立的虚拟内存空间。简单来说,进程是指在系统中正在运行的一个应用程序,每一个程序都是一个进程,并且进程之间是相互独立的,每个进程均运行在其专业且受保护的内存空间内。

2.线程

是程序执行流的最小单元,是系统独立调度和分派CPU的基本单位。

一个进程中至少有一条线程,即主线程。创建线程的目的就是为了开启一条新的执行路径,运行指定的代码,与主线程中的代码同时执行。

3.多线程

计算机同一个时间执行多个线程,进而提升整体处理性能。

原理:

(1)同一时间,CPU只能处理1条线程,只有一条线程在工作。

(2)多线程并发执行,其实是CPU快速的在多条线程中切换(调度)。

(3)如果CPU调度线程的速度够快,就造成多线程同时执行多假象。

优点:

(1)能适当提高程序的执行效率。

(2)能适当提高资源的利用率(CPU,内存利用率)。

缺点:

(1)开启新线程需要占用一定的内存空间(默认情况下,主线程占1M,子线程占512K)。如果开启大量的线程,会占用大量的内存空间,降低程序的性能。

(2)线程越多,CPU在线程间切换(调度)上的开销就越大。

注:主线程栈区的1M特别宝贵。不能杀掉一个线程!但可以暂停、休眠。


三、NSThread的使用

1.线程的创建

NSThread创建线程有以下三种方法:

[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]

NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

NSThread对象的常见属性:

NSThread类方法:

(1)当前线程:

int number = [NSThread currentThread];

number == 1 表示主线程,number != 1表示后台线程

(2)阻塞方法:

休眠到指定时间

[NSThread sleepUntilDate:[NSDate date]];

休眠指定时长

[NSThread sleepForTimeInterval:4.5];

(3)其他类方法:

退出线程

[NSThread exit];

当前线程是否为主线程

[NSThread isMainThread];

是否多线程

[NSThread isMultiThreaded];

返回主线程的对象

NSThread *mainThread = [NSThread mainThread];

2.线程的状态

线程的状态如下图:

(1)新建:实例化对象

(2)就绪:向线程对象发送start消息,线程对象被加入"可调度线程池"等待CPU调度,detach方法和performSelectorInBackground方法会直接实例化一个线程对象并且加入"可调度线程池"。

(3)运行:CPU负责调度"可调度线程池"中线程的执行,线程完成执行之前,其状态可能在"就绪"和"运行"之间切换。

(4)阻塞:当满足某个条件时,可以使用休眠或锁阻塞线程执行,方法有sleepForTimeInterval,sleepUntilDate,@synchronized(self)x线程锁。线程进入阻塞状态下时,会被从"可调度线程池"中移出,CPU不再调度。

(5)死亡:死亡后线程对象的isFinished属性为YES,如果是对线程发送cancel消息,线程对象的isCenceled属性为YES,死亡后stackSize==0,内存空间被释放。

2.多线程的安全问题

多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱,从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。 UIKit中几乎所有控件都不是线程安全的,因此需要在主线程中更新UI.

解决多线程安全问题:

(1)互斥锁

 注意:锁定1份代码只用1把锁,用多把锁是无效的

@synchronized(锁对象) { 需要锁定的代码  }

使用互斥锁,在同一时间,只允许一条线程执行锁中的代码。因为互斥锁的代价十分昂贵,所以锁定的代码范围应该尽可能吧小,只要锁住资源读写部分的代码即可。

(2)使用NSLock对象

(3)atomic加锁

OC在定义属性的时候有nonatomic和atomic两种选择。

atomic:原子属性,为setter方法加锁(默认就是atomic)。线程安全,但需要消耗大量资源。

nonatomic:非原子属性,不会为setter方法加锁。非线程安全,但效率高。

atomic加锁原理:

ios开发的建议:

所有属性都声明nonatomic。

尽量避免多线程抢夺同一块资源。

尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动端的压力。


四、GCD的使用

GCD(Grand Central Dispatch)伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。GCD会自动利用更多的CPU内核,会自动管理线程的生命周期(线程创建,调度任务,线程销毁),只需要告诉GCD想要如何执行什么任务,不需要编写任何线程管理代码。

一些专业术语:

dispatch:调度/派遣

queue:队列,用来存放任务的先进先出(FIFO)的容器。

sync:同步函数,只是在当前线程中执行任务,不具备开启新线程的能力。

async:异步函数,可以在新的线程中执行任务,具备开启新线程的能力。

concurrent:并发,多个任务同时进行。

串行:一个任务执行完毕后,再执行下一个任务。

1.GCD中的核心概念:

任务: 任务就是要在线程中执行的操作。我们将要执行的代码用block封装好,然后将任务添加到队列容器中,并指定任务的执行方式。等待CPU从队列中取出任务放到对应的线程中执行。

队列

串行队列:一次只调度一个任务,一个任务完成后再调度下一个任务。

并发队列:可以同时调度多个任务,调度任务的方式,取决于执行任务的函数,并发功能只有在异步函数下才有效。异步情况下,开启的新线程极限数量由GCD底层决定。

如果在MRC下需要使用dispatch_release释放队列:

主队列:负责在主线程上调度任务,如果在主线程上有任务执行,会等待主线程空闲后再进行调度。主队列用于UI以及触摸事件等的操作。

全局并发队列: 由苹果API提供的,方便程序员使用多线程。

全局并发队列的优先级:

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 // 后台优先级

全局并发队列与并发队列的区别:

(1)全局并发队列没有队列名称。

(2)在MRC中,全局并发队列不需要手动释放。


执行任务的函数

(1)同步函数(dispatch_sync)

任务被添加到队列后,队列中的任务一个接着一个执行。

在主线程中,向主队列添加同步任务,会造成死锁。

在其他线程中,向主队列添加同步任务,则会在主线程中同步执行。

(2)异步函数(dispatch_async)


GCD的其他用法

(1)延时执行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    // 2秒后异步执行这里的代码...

});

(2)一次性执行

应用场景:保证某段代码在程序运行过程中只被执行一次,在单例模式中经常被用到。

(3)调度组(队列组)

五、NSOperation

NSOperation是苹果推荐的并发技术,它提供了一些GCD不是很好实现的功能。NSOperation是基于GCD的面向对象的OC语言封装。相比GCD,NSOperation的操作更简单。NSOperation是一个抽象类,不能直接使用,而是使用它的子类。苹果为我们提供了其两个子类:NSInvocationOperation,NSBlockOperation.以及继承NSOperation的自定义子类。

NSOperation的使用常常是配合NSOperationQueue来进行的。只要使用NSOperation子类创建的实例就能添加到NSOperationQueue中,一旦添加到队列,操作就会自动异步执行。如果没有添加到队列,而是使用start方法,则会在当前线程中执行。

(1)NSInvocationOperation

直接创建一个NSInvocationOperation对象,然后调用start方法会直接在主线程中执行。

添加到NSOperationQueue中:

(2)NSBlockOperation

NSBlockOperation与NSInvocationOperation用法相同,只是创建的方式不同,它不需要去调用方法,而是直接使用代码块。这也使得NSBlockOperation比NSInvocationOperation更流行。

(3)NSOperationQueue的一些高级操作

NSOperationQueue的高级操作有:队列的挂起,队列的取消,添加操作的依赖关系和设置最大并发数量。

最大并发数:

线程的挂起:

取消队列里的所有操作:

 六、三种线程技术比较

1.NSThread

优点:NSThread比其他两个轻量级,使用简单。

缺点:需要管理自己的线程生命周期、加锁、睡眠以及唤醒等。

2.GCD

GCD是ios4.0以后才出现的并发技术

使用方式:将任务添加到队列(串行/并行(全局))中,指定执行任务的方法(同步函数sync,异步函数async)。

NSOperation无法做到的:延迟执行,队列组(NSOperation实现会比较复杂)。

3.NSOperation

NSOperation在ios2.0时就出现了(当时不好用,后苹果对其改造)

使用方式:将操作(异步执行)添加到队列(并发/全局)中。

提供了GCD不好实现的功能:最大并发数、取消所有任务、依赖关系。


GCD是比较底层的封装,我们知道较低层的代码一般性能都是比较高的,相对于NSOperationQueue。所以追求性能,而功能够用的话就可以考虑使用GCD。如果异步操作的过程需要更多的用户交互和被UI显示出来,NSOperationQueue会是一个好选择。如果任务之间没有什么依赖关系,而是需要更高的并发能力,GCD则更有优势。

尾语:

高德纳的教诲:“在大概97%的时间里,我们应该忘记微小的性能提升。过早优化是万恶之源。”只有Instruments显示有真正的性能提升时才有必要用低级的GCD。

你可能感兴趣的:(ios多线程详解)