iOS面试题(多线程篇)

发布于 2021-09-07 11:53 ,所属分类:2021面试经验技巧分享

1、进程和线程的区别?


(1)进程是个静态的容器,可以理解为正在执行的应用程序实例,它里面容纳了很多个线程,线程则是一系列方法的线性执行路径(CPU调度的基本单位)。


(2)进程拥有独立的资源空间(资源分配基本单位),共享起来比较复杂,常使用IPC方式进行同步,同步起来简单,线程间共享所属进程空间,资源共享简单但同步复杂,常使用加锁等方式进行同步。


(3)进程崩溃不会影响其他进程,一个线程崩溃则会导致整个进程崩溃


2、iOS中多线程有几种实现方式?分别有什么区别?


(1)pthread(POSIX Threads):一套C语言编写的跨平台多线程API,使用难度大,需要手动管理线程生命周期。


(2)NSThread:面向对象操作线程,使用相对简单,需要手动管理线程生命周期。


(3)GCD:苹果多核编程解决方案,使用起来非常方便。需要自己实现如:限制并发数,任务间的依赖等功能。自动管理线程生命周期。


(4)NSOperation:基于GCD的封装,面向对象操作线程,提供了比GCD更丰富的API:限制最大并发数,设置任务依赖关系。但是它不能直接使用,因为它是一个抽象类,可以继承它或者使用系统定义NSInvocationOperation或NSBlockOperation。自动管理线程生命周期。


3、NSOperationQueue和GCD有什么区别?


(1) GCD底层是C语言构成的API。NSOperationQueue及相关对象是Objc对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构。而NSOperation作为一个对象,为我们提供了更多的选择。


(2) 在NSOperationQueue中,取消任务非常方便,而GCD没法停止已经加入queue的block。


(3) NSOperation能够方便的设置依赖关系,还能设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行。在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务优先级也需要大量复杂代码。

NSOperationQueue还可以设置最大并发数,GCD则需要自己实现。


(4)NSOperation任务状态属性支持KVO,可以通过KVO来监听operation的就绪、取消、执行中、执行完成等状态。GCD则无法判断当前任务执行状态。


4、iOS中线程间如何通信?


线程间通信主要是线程间的同步:

(1)GCD:dispatch_group_t、dispatch_barrier、dispatch_semaphore

(2)NSOperationQueue:任务间添加依赖关系控制同步

(3)线程锁:NSLock、NSRecursiveLock等


涉及到线程异步数据传递:

(1)Mach port:NSMachPort结合RunLoop实现


  • 创建NSMachPort对象并设置代理

  • 将NSMachPort对象添加到RunLoop中

  • 实现NSMachPortDelegate方法

  • 创建一个新线程,在新的线程中调用之前创建的NSMachPort对象,往对应的RunLoop发送消息


(2)NSObject 对象相关API:


performSelector: onThread: withObject:waitUntilDone:modes:performSelectorOnMainThread:withObject:waitUntilDone:modes:......



5、iOS中线程有哪几种状态?


新建、就绪、运行、阻塞、死亡


At the application level, all threads behave in essentially the same way as on other platforms. After starting a thread, the thread runs in one of three main states: running, ready, or blocked.Ifathreadisnotcurrentlyrunning,itiseitherblockedandwaitingforinputoritisreadytorunbutnotscheduledtodosoyet.Thethreadcontinuesmovingbackandforthamongthesestatesuntilitfinallyexitsandmovestotheterminatedstate.



6、主队列一定在主线程执行吗?


主队列一定在主线程执行,但主线程执行的任务不一定都是主队列的任务。

在SDWebImage中有下面这么一段宏定义,它就是为了保证任务始终在主队列中执行。


#ifndef dispatch_main_async_safe#define dispatch_main_async_safe(block)\    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\        block();\    } else {\        dispatch_async(dispatch_get_main_queue(), block);\    }#endif



7、什么场景下会出现死锁?


同步+串行(主线程)

场景一:同步+主队列[self task1];dispatch_sync(dispatch_get_main_queue(), ^{[self task2];    NSLog(@"同步线程执行主队列任务");});[self task3];
1)执行task12)阻塞同步线程,把task2加入到主队列的队尾3)task3需要等待task2执行完成后执行,但是此时task2又排在task3后面,所以造成了死锁
场景二:异步串行队列嵌套同步串行队列
dispatch_queue_t myQueue = dispatch_queue_create("com.bg.sQueue", DISPATCH_QUEUE_SERIAL);[self task1];dispatch_async(myQueue, ^{ [self task2]; dispatch_sync(myQueue, ^{ [self task3]; }); [self task4];});[self task5];
1)执行task12)执行task53)执行task24)阻塞同步线程,把task3加入到队列myQueue的队尾5)task4需要等待task3执行完成后执行,但是此时task3又排在task4后面,所以造成了死锁
场景三:信号量阻塞主线程
dispatch_semaphore_t sem = dispatch_semaphore_create(0);dispatch_async(dispatch_get_main_queue(), ^{ dispatch_semaphore_signal(sem); NSLog(@"the sem +1");});dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);NSLog(@"the sem -1");
主线程中dispatch_semaphore_wait一直等着dispatch_semaphore_signal改变信号量(+1操作),但是dispatch_semaphore_wait却阻塞了主线程导致dispatch_semaphore_signal无法执行,从而造成了死锁。


8、多线程中dispatch_barrier_async为什么不能用全局队列?


dispatch_barrier_async必须用自定义的DISPATCH_QUEUE_CONCURRENT队列,如果使用全局队列或同步队列,则起不到栅栏函数的作用,相当于dispatch_async。


9、这段代码会有什么问题:


for (int i = 0; i < 100000; i++) {     NSString *queueN = [NSString stringWithFormat:@"c%dqueue", i];     dispatch_queue_t cQueue = dispatch_queue_create(queueN.UTF8String, DISPATCH_QUEUE_CONCURRENT);     dispatch_async(cQueue, ^{            NSLog(@"the queue = %s", dispatch_queue_get_label(cQueue));        });}


上述代码会一下子创建很多线程,造成线程爆炸。类似这种多线程循环执行任务场景,可以使用dispatch_apply来实现。因为dispatch_apply的线程全部由GCD管理,从而避免了手动创建线程爆炸问题。


//解决线程爆炸方案dispatch_queue_tqueue=dispatch_queue_create("xxQueue",DISPATCH_QUEUE_CONCURRENT);dispatch_apply(100000, queue, ^(size_t t) {        NSLog(@"the current thread = %@, t = %ld", NSThread.currentThread, t);});


10、GCD的任务能取消吗?


(1)iOS8以后,使用dispatch_block_create创建的block任务,可以通过dispatch_block_cancel来取消。


(2)一般我们会直接使用dispatch_async这种函数直接添加任务,面对这种没有使用dispatch_block_create创建block任务的场景,则可以自定义一个全局的BOOL值变量,然后在block中通过该值来判断是否要继续执行前任务。


11、如何实现一个常驻线程?(线程保活)


可参照 AFNetworking2 的源码(基于NSURLConnection,需要在后台接收并处理代理回调),实现一个常驻线程:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {    @autoreleasepool {        [[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }}
+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start];}); return _networkRequestThread;}


实现一个常驻线程需要注意以下几个要点:


(1)线程开启后,任务执行完成,线程就销毁了,所以实现常驻线程就需要它一直运行(RunLoop),线程和RunLoop是一一对应的关系,主线程默认开启RunLoop,子线程需要手动开启RunLoop。


(2)RunLoop启动前必须保证有输入源(Source/Observer/Timer),所以这里在执行[runLoop run]方法之前添加了NSMachPort对象,用来保证RunLoop不退出。


(3)[runLoop run]方法会开启一个无限循环来处理输入源的数据,就算从RunLoop中手动移除所有已知的输入源和计时器,仍不能保证RunLoop循环会退出,如果你需要终止RunLoop,你就不应该直接使用 run 方法。不过我们可以直接使用runMode:beforeDate:方法。它只会运行一次循环,可以通过自定义变量来控制是否需要运行或终止。


如 ReactiveObjC 源码所示:


do {[NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];} while (!done);


12、如何解决多线程安全问题?NSLock是什么锁?


多线程安全问题可以通过加锁来解决,下面是常见锁的类型:


互斥锁:@synchronized、NSLock、pthread_mutex实现


递归锁:NSRecursiveLock


信号量:dispatch_semaphore_t


条件锁:NSCondition、NSConditionLock


自旋锁:OSSpinLock:存在优先级反转的问题,目前已经弃用,使用os_unfair_lock(互斥锁)代替


根据objc源码可知 atomic老版本实现是自旋锁(spin_lock_t),新版实现是互斥锁(os_unfair_lock)


如果对性能要求高,可以优先使用信号量。


13、自旋锁和互斥锁有什么区别?


自旋锁,线程会一直处于忙等状态,需消耗CPU资源


互斥锁,线程会挂起处于休眠状态,不会消耗CPU资源


14、如何用GCD实现任务间的依赖?


15、如何用GCD去设计控制最大并发数的实现?


16、如果需要同时开启3个线程,并按顺序执行任务,使用GCD如何实现?



更多答案,请点击“”。


参考资料:


  • 线程编程指引:

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/AboutThreads/AboutThreads.html#//apple_ref/doc/uid/10000057i-CH6-SW2

  • SDWebImage:https://github.com/SDWebImage/SDWebImage

  • AFNetworkinghttps://github.com/AFNetworking/AFNetworking

  • ReactiveObjChttps://github.com/ReactiveCocoa/ReactiveObjC


相关资源