2022iOS面试题之多线程
GCD特点:
1、GCD是基于c语言的用于多核的并行运算
2、GCD会自动利用更多的CPU内核(比如双核、四核)
3、GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
4、程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
队列:串行队列:会顺序执行
并行队列:可以并行执行
全局队列:系统创建,全局并发队列
主队列:主队列与主线程是绑定的
只要是同步的方式提交任务,无论是串行还是并发,就会在同一线程去执行。
同步:只能在当前线程中执行任务,不具备开启新线程的能力
异步:可以在新线程中执行任务,具备开启新线程的能力
———————————————————————————————
死锁
原因:主队列里sync 同步执行,就是会先阻塞当前线程,直到block 当中的代码执行完毕,但是block在viewdidload里面,block需要等待viewdidload执行完结束才能继续,但是viewdidload需要等待block执行完才能结束。
可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
- (void)viewDidLoad { [super viewDidLoad]; //会死锁,都在主队列里同步执行 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"deallock"); }); } 解决 - (void)viewDidLoad { [super viewDidLoad]; //不会死锁,serialQueue虽然是串行队列,但不是主队列,serialQueue和viewDidLoad(在主队列执行)的执行队列不同就不会等待, dispatch_sync(serialQueue, ^{ NSLog(@"deallock"); }); }
———————————————————————————————
面试题:多读单写的GCD实现?异步栅栏到并发队列方案[就是像筑起一个栅栏一样,将队列中的多组线程任务分割开]
-(id)objectForKey: (NSString *) key{ __block id obi:
dispatch_queue_t concurrent_queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);
//同步读取指定数据,单读 dispatch_sync(concurrent_queue, ^{ obj= [userCenterDic obiectForKey: key];
}); return obi;
} -(void)setobject: (id)obj forKey: (NSString *) key{
dispatch_queue_t concurrent_queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);
//异步栅栏调用设置数据,多写
dispatch_barrier_async (concurrent_queue, ^{
[userCenterDic setobiect:obi forKey: key];
});
}
区别?
1、dispatch_barrier_sync需要等待自己的任务(barrier)结束之后,才会继续添加并执行写在barrier后面的任务(4、5、6),然后执行后面的任务
2、dispatch_barrier_async将自己的任务(barrier)插入到queue之后,不会等待自己的任务结束,它会继续把后面的任务(4、5、6)插入到queue,然后执行任务。
注意:在使用栅栏函数时.使用自定义队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用。
—————————————————————————————————
面试题:A/B/C任务并发执行完成后,再调用D任务?
在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。
使用dispatch_group 队列组。
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (NSInteger i = 0; i < 10; i++) { dispatch_group_async(group, concurrentQueue, ^{ sleep(1); NSLog(@"%zd:网络请求",i); }); } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"刷新页面"); });
——————————————————————————————
使用信号量,控制多线程下的同步操作。(作用:保证关键代码段不被并发调用。)
1. 初始化信号(initialize/create) 2. 发信号(signal/post) 3. 等信号(wait/suspend) 4. 释放信号(destroy) dispatch_semaphore_t sem = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务1:%@",[NSThread currentThread]); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务2:%@",[NSThread currentThread]); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任务3:%@",[NSThread currentThread]); });
———————————————————NSOperation——————————————
特性:
1.Dependency 设置依赖,优先级 2.NSOperation 有如下几种的运行状态:状态控制 * Pending * Ready * Executing * Finished * Canceled 除 Finished 状态外,其他状态均可转换为 Canceled 状态。 3. maxConcurrentOperationCount,默认的最大并发 operation 数量是由系统当前的运行情况决定的(来源),我们也可以强制指定一个固定的并发数量.
4、可以很方便的取消一个操作的执行。
5、使用KVO观察对操作执行状态的更改:isExecuting、isFinished、isCancelled。
NSOperation的任务状态
isReady 当前任务是否处于就绪状态
isExecuting 当前任务是否处于正在执行中状态
isFinished 当前任务是否已执行完毕
isCancelled 当前任务是否已取消
怎么去控制状态?重写main方法的话,底层帮我们自动控制的。重写start方法才是我们控制状态
1.如果只重写了NSOperation的main方法,底层会为我们控制变更任务执行完成状态,以及任务退出(后续线程的退出和NSOperation的退出) 2.如果重写了NSOperation的start方法,需要我们自行控制任务状态,在合适的时机去修改对应的isFinished等 查看NSOperation的start方法源码,理解上面两点 start方法内,首先创造一个自动释放池,然后获取线程优先级 做一系列的状态异常判断,然后判断当前状态是否isExecuting 如果不是,那么我们手动变成isExecuting,然后判断当前任务是否有被取消 若未被取消就调用NSOperation的main方法 再之后,调用NSOperation的finish方法。finish 方法中:在内部通过KVO的方式去变更isExecuting状态为isFinished状态 之后调用自动释放池的release 所以系统是在start方法里面为我们维护了任务状态的变更,若重写start,则没人帮我们维护了,只能自己手动维护
kvo监听状态属性的变化,来确定是否需要移除的,通过 KVO 监测 isExecuting 和 isFinished 这几个变量,来监测 Operation 的完成状态的
//设置优先级最高 op1.qualityOfService = NSQualityOfServiceUserInteractive; //设置依赖 : 列如:下载 解压 升级完成 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"下载"); }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ [NSThread sleepForTimeInterval:2.0]; NSLog(@"解压"); }]; NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"升级完成"); }]; //设置操作间的依赖 [op2 addDependency:op1]; [op3 addDependency:op2]; //会发生循环依赖 ,什么都不操作 //操作添加到队列中 [self.queue addOperations:@[op1,op2] waitUntilFinished:NO]; //依赖关系可以跨队列 [[NSOperationQueue mainQueue] addOperation:op3];
GCD 与 NSOperation 的对比
* 首先要明确一点,NSOperationQueue 是基于 GCD 的更高层的封装.
* 从易用性角度,GCD 由于采用 C 风格的 API,在调用上比使用面向对象风格的 NSOperation 要简单一些。
* 从对任务的控制性来说,NSOperation 显著得好于 GCD,和 GCD 相比支持了 Cancel 操作(注:在 iOS8 中 GCD 引入了 dispatch_block_cancel 和 dispatch_block_testcancel,也可以支持 Cancel 操作了),支持任务之间的依赖关系,支持同一个队列中任务的优先级设置,同时还可以通过 KVO 来监控任务的执行情况。这些通过 GCD 也可以实现,不过需要很多代码,使用 NSOperation 显得方便了很多。
* 效率,直接使用 GCD 效率确实会更高效,NSOperation 会多一点开销,但是通过 NSOperation 可以获得依赖,优先级,继承,键值对观察这些优势
* 从第三方库的角度,知名的第三方库如 AFNetworking 和 SDWebImage 背后都是使用 NSOperation,也从另一方面说明对于需要复杂并发控制的需求,NSOperation 是更好的选择
————————————————————————————————————————————————
线程间通信:切换到主线程[NSOperationQueue mainQueue]
———————————————————NSThread———————————————————————————
NSThread在实际开发中比较常用到的场景就是去实现常驻线程。
将耗时操作放在子线程中,并且在子线程中开启runloop,并使子线程常驻,这样就能不停的执行耗时操作,并且不会影响到主线程啦,滑动tableview很丝滑
NSThread *thread = [[NSThread alloc] initWithBlock:^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.35 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"1234567");
static int count = 0;
[NSThread sleepForTimeInterval:1];
//休息一秒钟,模拟耗时操作
}];;
[self.timer fire];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
如何保证线程安全?
NSOperation的线程安全
和其他多线程方案一样,解决NSOperation多线程安全问题,可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。
@synchronized、
NSLock、
NSRecursiveLock、
NSCondition、
NSConditionLock、
pthread_mutex、
dispatch_semaphore、
OSSpinLock等等各种方式。
@synchronized一般在创建单例对象的时候使用,保证创建的对象是唯一的,但是性能没有dispatch_once_t好。
@implementation XXClass //@synchronized来实现 + (id)sharedInstance { static XXClass *sharedInstance = nil; @synchronized(self) { if (!sharedInstance) { sharedInstance = [[self alloc] init]; } } return sharedInstance; } //dispatch_once_t来实现 @implementation XXClass + (id)sharedInstance { static XXClass *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }
@synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改,保证代码的安全性。也就是包装这段代码是原子性的,安全的。
这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其他线程访问,起到保护线程安全的作用
1、
atomic:变量默认是有该有属性的,这个属性是为了保证在多线程的情况下,编译器会自动生成一些互斥加锁的代码,避免该变量的读写不同步的问题。
nonatomic:如果该对象无需考虑多线程的情况,这个属性会让编译器少生成一些互斥代码,可以提高效率。
2、使用GCD实现atomic操作:给某字段的setter和getter方法加上同步队列
3、使用NSLock
4、使用互斥锁
————————————————————————————————————————————————
//互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
@synchronized (self){}
//使用NSLock
NSLock* myLock=[[NSLock alloc]init]; NSString *str=@"hello"; [NSThread detachNewThreadWithBlock:^{ [myLock lock]; NSLog(@"%@",str); str=@"world"; [myLock unlock]; }]; [NSThread detachNewThreadWithBlock:^{ [myLock lock]; NSLog(@"%@",str); str=@"变化了"; [myLock unlock]; }];
输出结果不加锁之前,两个线程输出一样 hello;加锁之后,输出分别为hello 与world。
“自旋锁OSSpinLock” & “互斥锁”的异同:
* 共同点
都能够保证线程安全
* 不同点
互斥锁:如果其他线程正在执行锁定的代码,此线程就会进入休眠状态,等待锁打开;然后被唤醒
自旋锁:如果线程被锁在外面,那么就会用死循环的方式一直等待锁打开!
自旋锁OSSpinLock用于轻量级的数据计算,如int型的+1、-1,在内存管理里面,对引用计数器的加减就使用了自旋锁。
递归锁NSRecursiveLock,可以重入,不会造成死锁。
NSLock不可以重入,会造成死锁。
换成递归锁就不会导致死锁了。
线程间通信
对于线程间通信常见的是线程间同步控制,比如通过线程锁、GCD队列、NSOperationQueue操作队列;
若涉及到线程间同步传递数据,最有效的方式是通过共享的进程内存并结合线程锁来控制数据同步;
若涉及到线程间异步传递数据,可通过mach port或者performSelector:onThread:withObject:waitUtilDone:并结合runloop来实现。
传递数据大小而言,对于大容量数据,一般会存储到临时文件并传递文件描述符;
具体的线程间通信方式需要根据实际的使用场景来选择,如同步/异步、传递数据大小、单向/双向通信等。
performSelector实现线程间通信(实现子线程和主线程互相通信,前提是子线程需要保活。【https://www.jianshu.com/p/eae43bdb7eb8】)
NSPort实例 实现线程间通信
案列:图片下载的案列,在子线程下载图片,在主线程更新UI
其他:
PerformSelector 的实现原理?
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。