一、多线程的基本概念
1、进程(Process):
- 进程定义:进程可以理解成一个运行中的应用程序,是一个活动的实体。它是是系统进行资源分配和调度的基本单位,也是基本的执行单元。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。
- 进程状态:进程有三个状态,就绪、运行和阻塞。
1、运行态:进程占用CPU,并在CPU上运行。
2、就绪态:进程已经具备运行条件,但是CPU还没有分配过来。
3、阻塞态:进程因等待某件事发生而暂时不能运行。
2、线程(Thread):
- 线程定义:是进程的基本执行单元,一个进程对应多个线程。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
- 线程状态:线基本状态:新建状态、就绪状态、运行状态、阻塞 (等待/睡眠)状态、死亡状态。
1、新建状态:新创建一个线程对象。
2、就绪状态:该状态位于“可运行的线程池”中,只等待获取CPU的使用权,调用start执行。
3、运行状态:就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态:是线程因为某种原因放弃CPU使用权,暂时停止运行。
5、死亡状态:线程执行完了或者因异常退出了执行方法,该线程结束生命周期。
3、多线程、主线程、父线程和子线程:
- 多线程:在同一时刻,一个CPU只能处理1条线程,但CPU可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象。(应用层就理解为多任务);
- 主线程:可以认为,主线程是程序(App)的一条主线(或UI线程),一旦主线程结束,程序程序(App)就结束了,其他所有线程都结束,所以主线程是特殊的线程;
- 父线程:一个进程中可以有很多的线程,这个时候可以新创建一个线程,而在这个线程中可以在再来创建别的线程。这样的话,线程之间就可以层层嵌套,之前最初创建的线程称之为父线程;
- 子线程:父线程中创建的线程(嵌套的线程),称之为子线程。(ps:还有一种通俗说法:除了主线程之外的都叫子线程,要注意区别和理解普通子线程和嵌套子线程)。
4、主线程、父线程和子线程关系:
重点:
1、当父线程消亡的时候,子线程是不会消亡的,是会继续执行到结束。(因为在内核当中,线程都是独立的内核对象 )
2、 当主线程消亡的时候,所有线程都得死。
2、主线程是不能看作任何线程的父线程,因为不满足父子线程的特性 ,主线程具有特殊性。
二、任务与队列
1、任务(Task):
就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
同步执行(sync):
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 可以在新的线程中执行任务,具备开启新线程的能力。
注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
2、队列(Queue):
这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Queue):
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Queue):
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列的并发功能只有在异步(async)函数任务下才有效
3、iOS主队列、全局队列、自定义队列:
主队列:专门负责调度主线程度的任务,没有办法开辟新的线程。所以,在主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行。
主队列异步任务:现将任务放在主队列中,但是不是马上执行,等到主队列中的其它所有除我们使用代码添加到主队列的任务的任务都执行完毕之后才会执行我们使用代码添加的任务。
主队列同步任务:容易阻塞主线程,所以不要这样写。原因:我们自己代码任务需要马上执行,但是主线程正在执行代码任务的方法体,因此代码任务就必须等待,而主线程又在等待代码任务的完成好去完成下面的任务,因此就形成了相互等待。整个主线程就被阻塞了。
全局队列:本质是一个并发队列,由系统提供,方便编程,可以不用创建就直接使用。
自定义队列:除了主队列和系统提供的全局队列之外,用户自定义的串行或者并行队列统称为自定义队列(普通队列)。
注意:全局并发队列、主队列是两种特殊队列,由iOS系统(GDC)提供。全局并发队列可以作为普通并发队列来使用。
4、队列与任务组合:
我们有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:
1、 同步执行 + 并发队列
2、 异步执行 + 并发队列
3、 同步执行 + 串行队列
4、 异步执行 + 串行队列
实际上,刚才还说了两种特殊队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是主队列因为有点特殊,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了。
5、 同步执行 + 主队列(容易阻塞卡死)
6、 异步执行 + 主队列
三、iOS多线程方案
4种实现方案:
pthread:一套C的通用的多线程API,线程生命周期需程序员管理,很少使用。
NSThread:使用更加面向对象,可以直接操作线程对象,线程生命周期需要程序员管理。
GCD(Grand Central Dispatch):基于C语言的API,是苹果公司为多核的并行运算提出的解决方案,自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
NSOperation:是基于GCD的封装,使用更加面向对象,线程生命周期自动管理。
1、pthread
POSIX threads 的简称,POSIX 线程是一个 POSIX 标准线程,是一套纯C语言的通用API,线程的生命周期需要程序员自己管理,使用难度较大,所以在实际开发中通常不使用,我们可以在开源框架YY_Kit中(YYMemoryCache)看到大量使用pthread_mutex锁。
#include <sys/ipc.h> // 进程间通信。
#include <sys/msg.h> //消息队列。
#include <pthread.h> //包含thread 库。
运用大量的API操作,灵活多变,难用。
pthread_create(&thread, NULL, run, NULL);中各项参数含义:
- 第一个参数&thread是线程对象,指向线程标识符的指针
- 第二个是线程属性,可赋值NULL
- 第三个run表示指向函数的指针(run对应函数里是需要在新线程中执行的任务)
- 第四个是运行函数的参数,可赋值NULL
2、NSThread
第一种方式实例化:
NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(threadRun) object:nil];
//也可以使用另外两种初始化函数
NSThread *newThread=[[NSThread alloc]init]; NSThread *newThread= [[NSThread alloc]initWithBlock:^{ NSLog(@"initWithBlock"); }];
第二种方式类方法:
//这种方式创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:@"lcj"];
第三种方式隐式创建:
//用于线程之间通信,比如:指定任务在当前线程执行
//不传递参数指定函数在当前线程执行
[self performSelector:@selector(doSomething)];
//传递参数指定函数在当前线程执行
[self performSelector: @selector(doSomething:) withObject:tempStr];
//传递参数指定函数2秒后在当前线程执行
[self performSelector:@selector(doSomething:) withObject:tempStr afterDelay:2.0];
3、GCD(Grand Central Dispatch)
是基于C语言的API,充分利用设备的多核,旨在替换NSThread等线程技术。线程的生命周期由系统自动管理,在实际开发中经常使用。
GCD的优势:
GCD是苹果公司为多核的并行运算提出的解决方案;
GCD会自动利用更多的CPU内核(比如双核、四核);
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
另外其他的方法:
1、 GCD栅栏(dispatch_barrier)
当任务需要异步进行,但是这些任务需要分成两组来执行,第一组完成之后才能进行第二组的操作。这时候就用了到GCD的栅栏方法dispatch_barrier_async。
2 、GCD队列组( dispatch_group )
异步执行几个耗时操作,当这几个操作都完成之后再回到主线程进行操作,就可以用到队列组了。
所有的任务会并发的执行(不按序)。
所有的异步函数都添加到队列中,然后再纳入队列组的监听范围。
使用dispatch_group_notify函数,来监听上面的任务是否完成,如果完成, 就会调用这个方法。
4、NSOperation
NSOperation是基于GCD之上的更高一层封装,NSOperation需要配合NSOperationQueue来实现多线程。NSOperation实现多线程的步骤如下:
1、创建任务:先将需要执行的操作封装到NSOperation对象中。
2、创建队列:创建NSOperationQueue。
3、将任务加入到队列中:将NSOperation对象添加到NSOperationQueue中。
需要注意的是,NSOperation是个抽象类,实际运用时中需要使用它的子类,有三种方式:
1、使用子类NSInvocationOperation
2、使用子类NSBlockOperation定义继承自NSOperation的子类,
3、通过实现内部相应的方法来封装任务。
四、线程安全与线程锁
1、线程安全:
- 线程管理安全:使用pthread 、NSTread等线程手动管理的API需要注意。
- 多线程访问数据安全:多线程中同时对一个数据或内存块进行操作的行为(读、写、增、删、遍历等等)。
- 线程锁使用安全:滥用互斥锁导致死锁。
2、线程锁的原理分类:
- 互斥锁(Mutex Lock):互斥量是阻塞锁。如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁,不会占用CPU资源。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
iOS中互斥锁:
@synchronized,NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock
- 自旋锁( Spin Lock) :自旋锁是一种非阻塞锁。如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一直占用CPU资源,一旦被访问的资源被解锁,则等待资源的线程会立即执行。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。"原地打转"。
iOS中自旋锁:
atomic、OSSpinLock、dispatch_semaphore_t
3、两种锁适用于不同场景:
- 如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。
- 如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。
- 如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。
- 如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。互斥锁线程进入休眠状态既不占用CPU资源,但是为什么,互斥锁比自旋锁的效率低呢,是因为休眠、以及唤醒休眠比忙等更加消耗CPU资源。
4、线程锁的功能分类:
- 读写锁 ( rwlock ):是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
- 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
- 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
- 递归锁 (recursivelock):严格上讲递归锁只是互斥锁的一个特例,同样只能有一个线程访问该对象,但允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。如果是加锁的可以继续加锁,继续往下走,不同线程来访问这段代码时,发现有锁要等待所有锁解开之后才可以继续往下走。