一、知识结构分析
备注:
从上到下,更加面向对象,也就更容易使用。
多线程之间的关系
pthread是
POSIX
线程的APINSThread是Cocoa对pthread的封装
GCD和NSOperationQueue是基于队列的并发API
GCD是基于pthread和queue实现的
NSOperationQueue是对GCD的高级封装
GCD
NSOperation
AFNetWorking + SDWebImage框架多有使用
问题1:AFNetWorking为什么使用NSOperation,为什么不使用GCD?
解释:
此道题目也可以理解为NSOperation比GCD的有点。
1、NSOperation是基于GCD更高一层的封装,完全面向对象;比GCD更加简单易用,代码可读性高。
2、 FIFO队列,而NSOperationQueue中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。CGD不具备
3、添加操作之间的依赖关系,方便的控制执行顺序;GCD不具备。
4、可以很方便的取消一个操作的执行;CGD不具备。
5、使用 KVO 观察对操作执行状态的更改;CGD不具备。
-
NSThread
用于实现一个常驻线程
问题2:常驻线程有什么作用?
解释:
通常情况下,创建子线程,在里面执行任务,任务完成后,子线程会立刻销毁;如果需要经常子线程中操作任务,那么频繁的创建和销毁子线程会造成资源的浪费。
所以需要常驻线程。
-
多线程和锁
线程同步和资源共享
二、GCD
主要结构如下:
- 同步/异步、串行/并发
- dispatch_barrier_async
- dispatch_group
2.1、同步/异步、串行/并发
dispatch_sync(serial_queue,^{ //任务 });
dispatch_async(serial_queue,^{ //任务 });
dispatch_sync(concurrent_queue,^{ //任务 })
dispatch_async(concurrent_queue,^{ //任务 })
2.1.1、同步串行
问题3:主队列同步
解释:
是队列引起的循环等待,不是线程引起的循环等待。
详细解释如下:
1、ios中默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block相当于在主队列添加任务2。
5、viewDidLoad在主线程运行。
6、dispatch_sync说明任务2也在主线程运行。
7、任务1完成后才能执行任务2。
但是任务1还没有完成,就开始执行任务2,任务2有依赖任务1的完成,任务1依赖任务2的完成,造成死锁。
问题4:主队列异步
没有问题
解释:
虽然没有问题,但是主队列提交的任务,无论通过同步/异步方式,都要在主线程进行处理!!!
问题5:串行队列同步
解释:
不会有问题。
详细解释如下:
1、iOS默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block是在另一个串行队列中,可以看成任务2。
5、viewDidLoad在主线程运行。
6、dispatch_sync说明任务2也在主线程运行。
7、因为二者不是在同一个队列,不会存在死锁,但是任务2会延迟任务1执行。
2.2.2、同步并发
问题6:下面代码输出结果是:
解释
1、iOS默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、global_queue是全局并发队列,里面有任务2和任务3
5、viewDidLoad在主线程运行。
6、dispatch_sync说明global_queue中的任务也在主线程运行(会阻断线程,强制执行自己的)。
7、因为global_queue和主线程队列不是同一个队列,不会造成死锁。
8、因为global_queue是全局并发队列,一个任务不用管前面的任务是否执行完毕。所以任务2未完成时,可以执行任务3,然后执行任务2,都是在主线程执行。
2.2.3、异步串行
这段代码是经常使用的
代码分析:
1、ios中默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block相当于在主队列添加任务2。
5、viewDidLoad在主线程运行。
6、dispatch_async说明任务2在子线程运行,也就是不会阻挡任务1的运行。
7、任务1完成后才能执行任务2。
因为任务1在子线程运行,不会阻挡任务2,所以正常使用。
2.2.4、异步并发
问题7:以下代码输出结果:
解释
1、global_queue是全局队列,采用dispatch_async,所以会开辟一个子线程。
2、子线程的runLoop默认是不开启的,而performSelector:withObject:afterDelay是在没有runloop的情况下会失效,所以此方法不执行。
3、打印结果13。
2.3、dispatch_barrier_async()
2.3.1、场景
问题8:怎样利用CGD实现多读单写?
利用CGD提供的栅栏函数
解析:
- 读者、读者并发
- 读者、写者互斥
- 写者、写者互斥
可以理解为:
1、读处理之间是并发的,肯定要用并发队列。
因为读取操作,往往需要立刻返回结果,故采用同步。
这些读处理允许在多个子线程。
2、写处理时候,其余操作都不能执行。利用栅栏函数,异步操作。利用栅栏函数异步操作的原因:栅栏函数同步操作会阻塞当前线程,如果当前线程还有其它操作,则会影响用户体验。
核心代码如下:
@interface UserCenter()
{
// 定义一个并发队列
dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问
NSMutableDictionary *userCenterDic;
}
@end
// 多读单写模型
@implementation UserCenter
- (id)init
{
self = [super init];
if (self) {
// 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key
{
__block id obj;
// 同步读取指定数据
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 异步栅栏调用设置数据
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
2.3.2、dispatch_barrier_sync和dispatch_barrier_async区别
共同点:
- 它们前面的任务先执行完。
- 它们的任务执行完,再执行后面的任务。
不同点:
- dispatch_barrier_sync会阻止当前线程,等它的任务执行完毕,才能往下进行。
- dispatch_barrier_async不会阻塞当前线程,允许其它非当前队列的任务继续执行。
注意:
使用栅栏函数时,使用自定义队列才有意义,如果使用串行队列/系统的全局并发队列,这个栅栏函数就相当于一个同步函数
2.3、dispatch_group
问题9:使用CGD实现这个需求:A、B、C三个任务并发,完成后执行任务D。
// 创建一个group
dispatch_group_t group = dispatch_group_create();
// 异步组分派到并发队列当中
dispatch_group_async(group, concurrent_queue, ^{
});
//监听
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 当添加到组中的所有任务执行完成之后会调用该Block
});
三、NSOperation
3.1、NSOperation优点
- 添加任务依赖
- 任务执行状态控制
- 最大并发量(maxConcurrentOperationCount)
问题10:我们可以控制任务的哪些状态?
-
isReady
当前任务是否处于就绪状态 -
isExecuting
当前任务是否正在执行 -
isFinished
当前任务是否执行完成 -
isCancelled
当前任务是否被标记为取消(不是判断是否被取消,是标记)
是通过KVO进行控制的。
3.2、状态控制
问题11:我们怎么控制NSOperation的状态
- 如果只重写了main方法,底层控制任务执行状态以及任务退出。
- 如果重写了start方法,自行控制任务状态。
问题12:系统是怎样移除一个isFinished=YES的NSOperation的?
- 通过KVO
小结:
NSOperation: 主队列默认在主线程执行,自定义队列默认在后台执行(会开辟子线程)。
四、NSThread
4.1、启动流程
1、调用start()方法、启动线程。
2、在start()内部会创建一个pthread线程,指定pthread线程的启动函数。
3、在启动函数中会调用NSThread定义的main()函数。
4、在main()函数中会调用performSelector:函数,来执行我们创建的函数。
5、指定函数运行完成,会调用exit()函数,退出线程。
4.2、常驻线程
参考RunLoop
五、多线程与锁
问题13:iOS中都有哪些锁,你是怎样使用的?
解释:
5.1、@synchronized(互斥锁) 🌟🌟🌟
- 一般在创建单例对象的时候使用,保证在多线程环境下,创建的单例对象是唯一的。
5.2、 atomic(自旋锁)🌟🌟🌟
- 属性关键字
- 对被修饰对象进行原子操作(不负责使用)
备注:
原子操作:不会被线程调度打断的操作;这种操作一旦开始,就一直运行到结束,中间不会切换到另一个线程。
不负责使用:属性赋值时候,能够保证线程安全;对属性进行操作,不能保证线程安全。
例如:
@property (atomic) NSMutableArray *array;
self.array = [NSMutableArray array];//线程安全
[self.array addObject:obj];//线程不安全
5.3、 OSSpinLock(自旋锁)
- 循环等待访问,不释放当前资源。
- 用于轻量级数据访问,简单的int值 +1/-1操作。
- 使用场景:
- 内存引用计数加1或减1
- runtime也有使用到。
5.4、 NSLock(互斥锁)
蚂蚁金服面试题:
解释:
对同一把锁两次调用,由于重入的原因会造成死锁;解决办法就是使用递归锁(可以重入)。
5.5、 NSRecursiveLock(递归锁)(互斥锁)
5.6、 dispatch_semaphore_t(信号量)🌟🌟🌟
-
dispatch_semaphore_create(5)
创建信号量,指定最大并发数 -
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
等待信号量>=1。
如果当前信号量>=1,信号量减1,程序继续执行。
如果信号量<=0,原地等待,不允许程序往下执行。 -
dispatch_semaphore_signal(semaphore)
程序执行完毕,发送信号,释放资源,信号量加1。
5.6.1、dispatch_semaphore_create
5.6.2、dispatch_semaphore_wait
5.6.3、dispatch_semaphore_signal
小结:
1、锁分为互斥锁、自旋锁
2、互斥锁和自旋锁的区别
自旋锁: 忙等待。即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放
互斥锁: 会休眠。即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其它线程工作,直到被锁资源释放,此时会唤醒休眠线程。
问题14:iOS系统为我们提供了几种多线程技术?各自有什么特点?
解释:
GCD
用于一些简单的线程同步,包括子线程分派;还有就是解决类似于多读单写功能。NSOperation及NSOperationQueue
可以方便控制任务状态、添加依赖/移除依赖;多用于复杂线程控制:AFNetWorking和SDWebImage。NSThread
往往用于实现一个常驻线程。
总结:
概念理解
队列概念
队列是任务的容器-
串行队列、并发队列区别
串行队列: 一次仅仅调度一个任务,队列中的任务一个个执行。(一个任务完成后,再运行下一个任务)。
遵循FIFO原则:先进先出、后进后出。并发队列:不需要把一个任务完成后,再运行下一个任务。
仍然遵循FIFO原则,只是不需要等待任务完成。 并行、并发区别
并行:同一时刻,多条指令在多个处理器同时执行。
并发:同一个时刻,只能处理一条指令,但是多个指令被快速的轮换执行,达到了具有同时执行的效果。异步、同步区别
异步: 可以开启新的线程。
同步: 不可以开启新的线程,在当前线程运行。
同步异步、串行并行形象理解
这两对概念单独看起来,明白怎么回事;但是,一旦运用起来,总是不能得心应手。总的来说,就是不能将概念熟记于心,缺乏形象概念。
下面采用图解 + 文字进行表述:
一个队列(串行+并发)好比一个容器。
执行代码好比一个个任务。
同步异步好比任务的标签。
容器里面装有好多个打有标签的任务。
线程好比流水线的传送带,
所有的工作都是CPU在做,姑且将CPU比做操作工。
代码运行的时候,大家想象工厂的流水线的工作场景:
1、从容器(队列)中取出任务(执行代码),放到传送带上。
如果容器是串行队列,则完成一个,取出一个。
如果容器是并发队列,则一直不停的投放。
2、任务(执行代码)放到传送带(线程)的一刹那,CPU(操作工)看了一眼上面的标签:如果标签是同步,就将它放到当前传送带;如果标签是异步,就新增加一条传送带,然后把任务放上去(理解操作工无所不能,可以随意增加传送带)。
上面只是一个形象的比喻,加深对多线程理解。
小结:
从上面的分析可知:
1、串行队列任务之间相互包含,容易造成死锁;并发队列则不会。这种死锁称为队列死锁。
2、并发队列+异步,才会有多线程效果。
如果只有当前一个线程可以利用,并发队列中任务虽然可以快速取出分派,奈何只有一个线程(主干道),只能一个个排队执行。