多线程基础
进程与线程
什么是进程
- 进程是指在系统中正在运行的一个应用程序,它是程序执行的一个实例
- 程序运行时系统会创建一个进程,为其分配资源,而后将该进程放入进程就绪队列,进程调度器选中其的时候就会为其分配CPU时间,而后程序真正开始运行。
- 各个进程之间相互独立,每个进程在其专有的且受保护的内存空间中

iOS开发是单进程,即一个进程就是一个APP;而安卓开发可以多线程
什么是线程
- 线程是进程的基本执行单位,一个进程中所有的任务都是在线程中执行的
- 进程想要执行任务就必须要有线程,一个进程最少有一个线程
- 程序启动是会默认开启一条线程,这条线程被称为主线程或者UI线程
进程与线程的区别
- 进程是资源分配 的最小单位,线程是程序执行的最小单位
- 进程有自己的独立地址空间,每次启动一个进程,系统都会为其分配地址内存,建立数据表来维护代码段、堆栈段、数据段。这种操作十分昂贵;线程是共享进程中的数据的,使用相同的地址空间,所以CPU切换一个线程的时间比一个进程的花费要少的多,创建一个线程的开销也比进程要小很多。
- 线程之间的通县更加方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。
- 多进程程序更健壮,多线程的程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另一个进程造成影响,因为进程有自己独立的地址空间
下面放一张进程与线程之间的关系图来方便理解:

我们将iOS
系统比做商场,进程是商场其中的店铺,线程是店铺中的员工,这样就可以很好的理解这张图了。
线程和runloop的关系
-
runloop
与线程一一对应------一个runloop对应一个核心的线程,为什么说是核心的,这是由于runloop
是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里 -
runloop是来管理线程的
------ 当线程的runloop
被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务runloop
在第一次获取时被创建,在线程结束时被销毁- 对于主线程来说,
runloop
在程序一启动就默认创建好了 - 对于子线程来说,
runloop
是懒加载的 ------ 只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调
- 对于主线程来说,
影响任务执行速度的因素:cpu调度、线程的执行速度、队列情况、任务执行的复杂度、任务的优先级
多线程
多线程原理
- 对于单核CPU来说,同一时间内,CPU只能处理一条线程,也就是说只有一条线程在工作。
- 多线程同时执行的本质:CPU在多个任务之间进行快速的切换,由于CPU调度线程的时间足够快,所以造成了多线程"同时"执行的效果,其中切换的时间就是时间片。
时间片:又称为"量子(
quantum
)"或"处理器片(processor slice
)"是分时操作系统分配给每个正在运行的进程微观上的一段CPU
时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。简单来说就是:
CPU
时间片即CPU
分配给多个程序的时间,每个线程被分配一个时间段,称作它的时间片
。
多线程优缺点
优点
- 能适当提⾼程序的执⾏效率
- 能适当提⾼资源的利⽤率(如
CPU,内存
) - 线程上的任务执⾏完成后,线程会⾃动销毁
缺点
- 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占
512
KB,床架线程的时间大约为90毫秒) - 如果开启⼤量的线程,会占⽤⼤量的内存空间,降低程序的性能
- 线程越多,
CPU
在调⽤线程上的开销就越⼤,程序设计更加复杂,⽐如线程间的通信、多线程的数据共享
多线程生命周期
多线程的声明周期主要为五部分:新建-就绪-运行-阻塞-死亡:

下面我们来看看这五个阶段:
- 新建:通过创建线程的函数,创建一个新的线程
- 就绪 :线程创建完成以后,调用
start
方法,线程这个时候处于等待状态,等待CPU时间分配执行 - 运行 :当就绪的线程被调度并获得
CPU
资源时,便进入运行状态,run
方法定义了线程的操作和功能。 - 阻塞 :在运行状态的时候,可能因为某些原因导致运行状态的线程变成了
阻塞
状态,比如sleep
、等待同步锁
,线程就从可调度线程池
移出,处于了阻塞
状态,这个时候sleep
到时、获取同步锁
,此时会重新添加到可调度线程池
。唤醒的线程不会立刻执行run
方法,它们要再次等待CPU
分配资源进入运行状态。sleepUntilDate
:阻塞当前线程,直到指定的时间位置,即休眠到指定的时间。sleepForTimeInterval
:在给定的时间间隔内休眠线程,即指定休眠时长。- 同步锁:
@synchronized(self):
- 销毁:如果线程正常执行完毕后或者线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。
线程的
exit
和cancel
说明
exit
:一旦强行终止线程,后续的所有代码都不会执行cancel
:取消当前线程,但是不能取消正在执行的线程
线程池的原理

【第一步】判断核心线程池是否都正在执行任务
- 返回NO,创建新的工作线程去执行
- 返回YES,进入【第二步】
【第二步】判断线程池工作队列是否已经饱满
- 返回NO,将任务存储到工作队列,等待CPU调度
- 返回YES,进入【第三步】
【第三步】判断线程池中的线程是否都处于执行状态
- 返回NO,安排可调度线程池中空闲的线程去执行任务
- 返回YES,进入【第四步】
【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)
AbortPolicy
:直接抛出RejectedExecutionExeception
异常来阻止系统正常运行CallerRunsPolicy
:将任务回退到调用者DisOldestPolicy
:丢掉等待最久的任务DisCardPolicy
:直接丢弃任务
实现多线程的方案

后面会详细介绍这些内容,这里先不做详细讲解。
线程安全问题
自旋锁与互斥锁
互斥锁
- 用于保护临界区,确保同一时间只有一条线程能够执行
- 若是代码中仅有一个地方需要加锁,大多都使用self,这样可以避免单独再创建一个锁对象
- 加互斥锁的代码,当新线程访问时,若是有其他线程正在执行锁定的代码,那么新代码就会进入休眠
注意:
- 互斥锁的锁定范围应尽量小一点,锁定范围越大,效率越差
- 能够加锁的任意
NSObject
对象- 锁对象一定要确保所有的线程都能够访问
自旋锁
- 自旋锁顾名思义,在获取锁之前是一个忙等的状态,即原地打转
- 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符
atomic
,本身就有一把自旋锁
- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用
死循环
的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
自旋锁和互斥锁相同与不同:
- 相同:在同一时间,保证只有一条线程执行任务,保证了相应同步的功能
- 不同:
- 互斥锁:发现其他线程执行,当前线程
休眠
(即就绪状态
),进入等待执行,即挂起。一直等其他线程打开之后,然后唤醒执行 - 自旋锁:发现其他线程执行,当前线程
一直询问
(即一直访问),处于忙等状态
,耗费的性能
比较高
- 互斥锁:发现其他线程执行,当前线程
当前任务状态比较短小精悍的时候,使用自旋锁;反之我们则使用互斥锁
原子锁与非原子锁
- atomic是原子属性,是为多线程开发准备的,是默认属性!
- 仅仅在属性的
setter
方法中,增加了锁(自旋锁)
,能够保证同一时间,只有一条线程
对属性进行写
操作 同一时间 单(线程)写多(线程)读
的线程处理技术
- Mac开发中常用
- 仅仅在属性的
- nonatomic 是非原子属性
没有锁
!性能高
!- 移动端开发常用
++尽量避免多线程抢夺同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力++
线程间通讯

这里笔者还没有看明白,先暂且引用笔者学习时的资料:

NSOperation
NSOperation
是个抽象类,依赖于子类NSInvocationOperation
、NSBlockOperation
去实现
使用的三种方式
NSInvocationOperation
objc
//处理事务
NSInvocationOperation* op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleinvocation:) object:@"CJ"];
//创建队列
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
//操作加入队列
[queue addOperation:op];
上述为基本使用
objective-c
NSInvocationOperation* op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleinvocation:) object:@"CJ"];
[op start];
直接处理事务,不添加隐形队列
注意:
这里我们不可以将[queue addOperation:op];
和[op start];
同时使用,这样会导致崩溃,这是由于线程的生命周期造成。
[queue addOperation:op]
已经将处理事务的操作任务加入到队列中,并让线程运行op start
将已经运行的线程再次运行造成线程混乱
NSBlockOperation
这里两个类比较来看,前者类似target
形式,后者类似block
形式。下面来展示一下:
objective-c
NSBlockOperation* bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务一------%@", [NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"任务二------%@", [NSThread currentThread]);
}];
bo.completionBlock = ^{
NSLog(@"完成了!!!");
};
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo];
NSLog(@"添加进去了");

这里由于NSBlockOperation
是异步执行,所以这里任务一、二完成的顺序并不确定。
这里我们使用addExecutionBlock
可以实现多线程:
objective-c
NSBlockOperation* bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务一------%@", [NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"任务------%@", [NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"2------------%@", [NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"3-----%@", [NSThread currentThread]);
}];
[bo start];
这里我们看看其运行结果:

这里我们可以看到任务一在主线程中执行,我们需要注意的是NSBlockOperation
创建时block中的任务是在主线程中执行,而运用addExecutionBlock
加入的任务是在子线程执行的。
自定义继承自NSOperation
的子类,通过实现内部的相应方法来封装任务
这里展示一下代码:
objective-c
//CJOperation.h
import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface CJOperation : NSOperation
@end
NS_ASSUME_NONNULL_END
//CJOperation.m
#import "CJOperation.h"
@implementation CJOperation
-(void) main {
NSLog(@"main -- %@", [NSThread currentThread]);
}
@end
//viewController.m
- (void)viewDidLoad {
[super viewDidLoad];
[self test14];
// [self test05];
}
#pragma mark-GCD的组合方式
-(void) test14 {
CJOperation* op1 = [[CJOperation alloc] init];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:op1];
}
运行结果:

NSOperationQueue
NSOperationQueue
有两种队列:主队列、其他队列。这里其他队列包含了串行与并发
- 主队列:任务在主线程上执行的
- 其他队列:加到非主队列中的任务默认就是并发,开启多线程
设置执行顺序
objective-c
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
for (int i = 0; i < 5; i++) {
[queue addOperationWithBlock:^{
NSLog(@"%@ -- %d", [NSThread currentThread], i);
}];
}

通过这段代码的执行结果来看,我们可以看到这些任务都是并发执行的,并没有固定的顺序执行
设置优先级
objective-c
NSBlockOperation* bo1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"第一个:%@ -- %d", [NSThread currentThread], i);
}
}];
//设置最低优先级
bo1.qualityOfService = NSQualityOfServiceBackground;
NSBlockOperation* bo2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"第二个:%@ -- %d", [NSThread currentThread], i);
}
}];
//设置最高优先级
bo2.qualityOfService = NSQualityOfServiceUserInteractive;
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo1];
[queue addOperation:bo2];
这里我们运行会发现第二个操作会在第一个操作之前执行,因为其优先级较高。

设置并发数
我们在使用GCD的时候,只能通过信号量来设置并发数,但是当我们使用NSOperation
的时候可以通过maxConcurrentOperationCount
来控制单词出队列去执行的任务数。
objective-c
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
for (int i = 0; i < 5; i++) {
[queue addOperationWithBlock:^{
NSLog(@"%@ -- %d", [NSThread currentThread], i);
}];
}
这个时候会按顺序执行,由于一次只能出队列去执行一个任务,这里就不展示运行结果了。
添加依赖
objective-c
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
NSBlockOperation* bo1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"hello");
}];
NSBlockOperation* bo2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"you good");
}];
NSBlockOperation* bo3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"I'm fine");
}];
[bo1 addDependency:bo2];
[bo2 addDependency:bo3];
[queue addOperations:@[bo1, bo2, bo3] waitUntilFinished:YES];
NSLog(@"Bye");
看看运行结果:

线程间通讯
当我们在使用NSOperation
也可以实现线程间通讯的效果:

任务的挂起、继续、取消
objc
//挂起
queue.suspended = YES;
//继续
queue.suspended = NO;
//取消
[queue cancelAllOperations];
但这里我们要注意的是,当我们挂起的瞬间如果已经将任务调度出了队列,那么这时候他们无法挂起,会被执行:
