iOS面试题 - 多线程和Runloop

作者: Longshihua | 来源:发表于2019-05-31 11:32 被阅读75次

    1、什么是多线程?

    多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

    在单核 CPU 时代,支持多线程的操作系统会通过分配 CPU 计算时间,来实现软件层面的多线程。创建线程,线程间切换都是有成本开销的。但由于多线程可以避免阻塞所造成的 CPU 计算时间浪费,所以多线程所带来的开销成本总体看来是值得的。任务一般都可以被拆分成多个子任务,如果一个子任务发生了阻塞,计算时间就可以分配给其他子任务。这样就提高了 CPU 的利用率。

    在多核 CPU 时代,就更好理解了。由于硬件上就支持多线程技术,就可以让多个线程真正同时地运行。如果任务能够被拆分,各个子任务就能并行地在 CPU 上运行,这就能显著加快运行速度。

    总结说来,多线程的目的是,通过并发执行提高 CPU 的使用效率,进而提供程序运行效率。

    优点:

    • 能适当提高程序的执行效率

    如:将耗时较长的操作(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,可以防止主线程的卡死

    • 可以发挥多核处理的优势,能提高资源利用率(CPU、内存利用率)

    缺点:

    • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    • 线程越多,CPU在调度线程上的开销就越大
    • 如果出现多个线程同时访问一个资源,会出现资源争夺等相关多线程问题

    2、进程、线程、串行、并行、并发、同步、异步

    • 进程和线程

    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

    当我们应用程序启动之后,默认会开启一条线程,称为主线程或UI线程,在主线程中我们可以描绘用户界面,处理触摸屏幕事件等。但是如果在主线程处理一些耗时任务,比如:同步下载图片、访问数据库,那么肯定是会妨碍主线程的执行,从而导致用户界面出现不更新,或者不能响应用户事件等影响体验的问题。

    串行和并行

    串行和并行描述的是任务和任务之间的执行方式.

    串行列队指在同一时间内,列队只能执行一个任务,当前任务执行完成后才能执行下一个任务,比如:任务A执行完了任务B才能执行, 它们俩只能顺序执行。

    并行列队允许多个任务在同一个时间同时进行,比如:任务A和任务B可以同时执行。

    串行列队的任务一定是按开始的顺序结束,而并行列队的任务不一定会按照开始的顺序结束

    同步和异步

    声明任务是同步执行还是异步执行。同步会把当前的任务加到列队中,等待任务执行完成,线程才会返回继续运行。也就是说,同步任务会阻塞线程。异步也会把当前的任务加到列队中,但它立刻会返回,无须等待任务执行完成,异步不会阻塞线程

    无论是串行列队还是并行列队,都可以执行同步或异步操作。注意,在串行列队中执行同步操作容易造成死锁,在并行列队中则不用担心这个问题。异步操作无论是在串行列队中执行还是并行列队中执行,都可能出现竞态条件的问题;同时,异步操作经常与逃逸闭包一起出现

    并行和并发

    并行是指两个或多个事件在同一时刻发生。多核CUP同时开启多条线程供多个任务同时执行,互不干扰。

    而并发是指两个或多个事件在同一时间间隔内发生,可以在某条线程和其他线程之间反复多次进行上下文切换,看上去就好像一个CPU能够并且执行多个线程一样,其实是伪异步。借助下图帮助理解

    1448936859347563.png

    3、iOS并发编程中的三大问题

    在并发编程中,一般会面对三个问题:竞态条件、优先倒置、死锁问题。对于iOS开发具体定义为:

    竞态条件:是指两个或两个以上线程对共享的数据进行读写操作时,最终的数据结构不确定的情况。

    比如:

    var num = 0
    DispatchQueue.global().async {
        for _ in 1...10000 {
            num += 1
        }
    }
    
    for _ in 1...10000 {
        num += 1
    }
    

    最后的计算结果num很可能小于20000,因为其操作为非原子操作。在上述两个线程对num进行读写时,其值会随着进程执行顺序的不同而长生不同的结果

    优先倒置:指低优先级的任务会因为各种原因先于高优先级任务执行。

    比如:

    let highProrityQueue = DispatchQueue.global(qos: .userInitiated)
    let lowProrityQueue = DispatchQueue.global(qos: .utility)
    
    let semaphore = DispatchSemaphore(value: 1)
    
    lowProrityQueue.async {
        semaphore.wait()
        for i in 0...10 {
             print(i)
        }
        semaphore.signal()
    }
    
    highProrityQueue.async {
        semaphore.wait()
        for i in 11...20 {
             print(i)
        }
        semaphore.signal()
    }
    

    上述代码如果没有semaphore,则高优先级的highProrityQueue会先执行,所以线程会先打印完11-20.而加了semaphore之后,低优先级的lowProrityQueue会先挂起semaphore,高优先级的highProrityQueue就只有等待semaphore被释放才能再执行打印操作

    也就是说,低优先级的线程可以锁上高优先级线程需要的资源,从而迫使高优先级的线程等待低优先级的线程。

    死锁问题:指两个或两个以上的线程,它们之间互相等待彼此停止执行,以获得某种资源,但是没有一方会提前退出的情况。

    比如:Operation相互依赖

    let operation1 = Operation()
    let operation2 = Operation()
    
    operation1.addDependency(operation2)
    operation2.addDependency(operation1)
    

    又比如:同一个串行列队中进行异步、同步嵌套

    let serialQueue = DispatchQueue.main
    serialQueue.async {
        print(1)
        serialQueue.sync {
            print(2)
        }
        print(3)
    }
    

    因为串行列队一次只能执行一个任务,所以,首先它会把异步闭包(block)中的任务派发执行,当进入闭包之后,同步操作意味着阻塞当前列队。而此时外部的闭包正在等待内部的闭包完成,而内部的闭包又阻塞其操作完成,即内部闭包在等待外部闭包完成。所以,相互等待造成了死锁。

    主线程使用同步操作也会造成死锁

    override func viewDidLoad() {
        super.viewDidLoad()
        print(1)
        DispatchQueue.main.sync {
             print(2)
        }
        print(3)
    }
    

    运行程序,因为死锁直接崩溃

    4、下面代码存在什么问题?

    - (void)viewDidLoad {
      [super viewDidLoad];
    
      UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
      label.text = @"wait 3 seconds...";
      [self.view addSubview:label];
    
      NSOperationQueue *queue = [[NSOperationQueue alloc]init];
      [queue addOperationWithBlock:^{
          [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
          label.text = @"ready to go";
      }];
    }
    

    上面代码的问题在于等了3秒之后,label并不会更新内容为ready to go注意:实际运行代码的时候,你可能看到其实内容发生了改变,但是子线程操作UI是会出现未知情况的。iOS中规定UI操作必须在主线程,上面block中是在子线程,所以即使看到内容发生改变也不用惊奇。

    解决方法

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperationWithBlock:^{
            NSLog(@"%@", [NSThread currentThread]);
            [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
            // 回到主线程
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
               label.text = @"ready to go...";
        }];
    }];
    

    5、GCD的一些常用的函数?

    • DispatchGroup

    DispatchGroup用于管理一组任务的执行,然后监听任务的完成,进而执行后续操作。比如:同一个页面发送多个网络请求,等待所有结果请求成功刷新UI界面。一般的操作如下:

    let queue = DispatchQueue.global()
    let group = DispatchGroup()
     
    group.enter()
    queue.async(group: group) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
             print("Task one finished")
             group.leave()
        })
    }
     
    group.enter()
    queue.async(group: group) {
        print("Task two finished")
        group.leave()
    }
     
    group.enter()
    queue.async(group: group) {
        print("Task three finished")
        group.leave()
    }
     
    group.notify(queue: queue) {
        print("All task has finished")
    }
    

    运行结果

    Task two finished
    Task three finished
    Task one finished
    All task has finished
    
    • barrier(栅栏)

    在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用barrier来等待之前任务完成,避免数据竞争等问题。 dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行dispatch_barrier_async 函数追加的处理,等dispatch_barrier_async追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

    let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
    queue.async {
        print("async1")
    }
    queue.async {
        print("async2")
    }
    queue.async(flags: .barrier) {
        print("async3")
    }
    queue.async {
        print("async4")
    }
    

    运行结果

    async2
    async1
    async3
    async4
    

    字典的读写操作

    private let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
    private var dictionary: [String: Any] = [:]
     
    public func set(_ value: Any?, forKey key: String) {
         // .barrier flag ensures that within the queue all reading is done
         // before the below writing is performed and
         // pending readings start after below writing is performed
         concurrentQueue.async(flags: .barrier) {
              self.dictionary[key] = value
         }
    }
     
    public func object(forKey key: String) -> Any? {
         var result: Any?
         concurrentQueue.sync {
             result = dictionary[key]
         }
     }
    

    通过在并发代码中使用barrier将能够保证写操作在所有读取操作完成之后进行,而且确保写操作执行完成之后再开始后续的读取操作。具体的详情看这里

    • Semaphore(信号量)

    对于信号量的具体内容,可以看博文。使用起来很简单,创建信号量对象,调用signal方法发送信号,信号加1,调用wait方法等待,信号减1。也适用信号量实现刚刚的多个请求功能。

    let queue = DispatchQueue.global()
    let group = DispatchGroup()
    let semaphore = DispatchSemaphore(value: 0)
     
    queue.async(group: group) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
            semaphore.signal()
            print("Task one finished")
        })
        semaphore.wait()
    }
    queue.async(group: group) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.8, execute: {
            semaphore.signal()
            print("Task two finished")
        })
        semaphore.wait()
    }
    queue.async(group: group) {
        print("Task three finished")
    }
     
    group.notify(queue: queue) {
        print("All task has finished")
    }
    

    运行结果

    Task three finished
    Task two finished
    Task one finished
    All task has finished
    
    • 循环执行任务 (dispatch_apply)
     DispatchQueue.concurrentPerform(iterations: 5) {
         print("\($0)")
     }
    
    • DispatchSource

    DispatchSource提高了相关的API来监控低级别的系统对象,比如:Mach ports, Unix descriptors, Unix signals, VFS nodes。并且能够异步提交事件到派发列队执行。

    简单的倒计时功能

    // 定时时间
    var timeCount = 60
    // 创建时间源
    let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
    timer.schedule(deadline: .now(), repeating: .seconds(1))
    timer.setEventHandler {
        timeCount -= 1
        if timeCount <= 0 { timer.cancel() }
        DispatchQueue.main.async {
            // update UI or other task
        }
    }
    // 启动时间源
    timer.resume()
    

    参考

    6、如何使用队列来避免资源抢夺?

    • 串行队列
    • 信号量
    • barrier
    • 加锁

    7、NSOperationQueue中有一个属性叫maxConcurrentCount即最大并发数。这里所谓的最大并发数指的是什么含义?

    这里的最大并发数指得是在队列中同时执行的任务的个数。有很多人会认为是所分配的线程的个数。其实不是的。因为线程的个数的多少取决于系统,系统会分配合适的线程数量来保证这些任务并发执行的,作为程序员我们是没办法控制的

    8、Thread中的Runloop的作用,如何使用?

    每个线程(Thread)对象中内部都有一个RunLoop对象用来循环处理输入事件,处理的事件包括两类:

    • 一是来自Input sources的异步事件
    • 一是来自Timer sources的同步事件

    RunLoop在处理输入事件时会产生通知,可以通过Core Foundation向线程中添加运行时观察者(run-loop observers)来监听特定事件,在监听的事件发生时做附加的处理工作。

    主线程的Run Loop会在App运行时自动运行,子线程中需要手动运行。Run Loop就是一个处理事件源的循环,我们可以控制这个Run Loop运行多久,如果当前没有事件发生,Run Loop会让这个线程进入睡眠状态(避免再浪费CPU时间),如果有事件发生,Run Loop就处理这个事件。

    如果子线程进入一个循环需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式,如果需要Timer,那么Run Loop就是必须的。

    开发中遇到的需要使用Run Loop的情况有:

    • 需要使用Port或者自定义Input Source与其他线程进行通讯。
    • 子线程中使用了定时器
    • 使用任何performSelector到子线程中运行方法
    • 使用子线程去执行周期性任务
    • NSURLConnection在子线程中发起异步请求

    Run Loop是一个线程里运行循环,并对收到的事件进行处理,

    • 保持线程长时间存活
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [runLoop run];
    
    • 用于定时器

    9、iOS中并发操作的方式

    iOS中并发操作的方式有:pthread、NSThread、GCD、Operation、performSelectors

    • pthread

    C语言级别的线程管理,一套通用的多线程API,适用于Unix\Linux\Windows等系统跨平台\可移植,需要程序员手动管理线程周期,在实际的iOS开发中很少使用

    • NSThread

    NSThreadpthread的进一步封装,但是开发中也很少使用。NSThread可以最大程度地掌握每一个线程的生命周期。但是,也需要开发者手动管理所有的线程活动,比如:创建、同步、暂停、取消等,其中手动加锁操作的挑战性很大。NSThread总体使用场景很小,基本上是在开服底层的开源软件或是测试时使用

    • GCD

    GCD是一种非常简单的并发编程方式。把要执行的任务封装到block就行了,然后只需要调用一个C函数,就可以实现并发。大部分情况下,开发者不需要维护自己的队列,只需使用系统提供的队列即可,更加节省了开发时间,提高开发效率。同时,GCD不需要开发者直接跟线程操作接触,所有的并发线程由系统提供并维护,大大简化了多线程管理的工作。系统会根据资源的多少来决定并发线程的数量,充分利用设备的多核,不会出现线程数量膨胀过多的情况,使整个App运行效率更高。

    由于GCD是一种C级别的API,故无法运用面向对象的很多特性。GCD采用block封装执行任务,比较适合小型的任务并发执行,对于一些大型的任务,代码量过大,会导致难以维护的尴尬。GCD没有对任务执行状态监听的机制,一旦分发任务,只能等待任务执行完成。

    GCD的使用

    • NSOperation

    NSOperation、NSOperationQueue是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue是基于 GCD 更高一层的封装,完全面向对象。NSOperation提供任务的封装,NSOperationQueue顾名思义,执行队列,可以自动实现多核并行计算,自动管理线程的生命周期,如果是并发的情况,其底层也使用线程池模型来管理。

    多线程 - NSOperation和NSOperationQueue

    • performSelectors

    NSObject提供了以 performSelector为前缀的一系列方法。它们可以让用户在指定线程中,或者立即,或者延迟执行某个方法调用。这个方法给了用户实现多线程编程最简单的方法。

    multithread-technology

    10、什么时候选择使用GCD,什么时候选择Operation?

    很少有其他技术能与GCD的同步机制相媲美。对于那些只需要执行一次的代码来说,也是如此,使用 GCDdispatch_once最为方便。然而,在执行后台任务时,GCD并不一定是最佳方式。还有一种技术叫做NSOperationQueue,它虽然与GCD不同,但是却与之相关,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作也能够并发执行。其与 GCD派发队列有相似之处,这并非巧合。“操作队列”(operation Queue)在 GCD 之前就有了,其中某些设计原理因操作队列而流行,GCD 就是基于这些原理构建的。实际上,从 iOS 4 与 Mac OSX 10.6 开始,操作队列在底层是用 GCD 来实现的。

    在两者的诸多差别中,首先要注意:GCD 是纯 C 的 API,而操作队列则是 Objective-C 的对象。在 GCD 中,任务用块来表示,而块是个轻量级数据结构。与之相反,“操作”(operation)则是个更为重量级的 Objective-C 对象。虽说如此,但 GCD 并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

    NSOperationQueue类的“addOperationWithBlock:”方法搭配 NSBlockOperation 类来使用操作队列,其语法与纯GCD方式非常类似。

    使用 NSOperation 及 NSOperationQueue 的好处如下:

    1、取消某个操作

    如果使用操作队列,那么想要取消操作是很容易的。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需要执行,不过,已经启动的任务无法取消。

    若是不使用操作队列,而是把块安排到GCD队列中,那就无法取消了。那套架构是 “安排好任务之后就不管了”(fire and forget)。开发者可以在应用程序层自己来实现取消功能,不过这样做需要编写很多代码,而那些代码其实已经由操作队列实现好了。

    2、指定操作间的依赖关系

    一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另外一个操作顺序执行完毕之后方可执行。比方说,从服务器下载并处理文件的动作,可以用操作来表示,而在处理其他文件之前,必须先下载 “清单文件”(manifest file)。后续的下载操作,都要依赖于先下载清单文件这一操作。如果操作队列允许并发的话,那么后续的多个下载操作就可以同时执行,但前提是它们所依赖的那个清单文件下载操作已经执行完毕。

    3、通过键值观测机制监控 NSOperation对象的属性

    NSOperation对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如:可以通过 isCancelled属性来判断任务是否已取消,又比如可以通过isFinished属性来判断任务是否已完成。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的任务,那么键值观测机制会很有用。

    4、指定操作的优先级

    操作的优先级表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法(scheduling algorithm)虽 “不透明”(opaque),但必然是经过一番深思熟虑才写成的。

    反之,GCD 则没有直接实现此功能的办法。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。而令开发者在GCD之上自己来编写调度算法,又不太合适,因此,在优先级这一点上,操作队列所提供的功能要比GCD更为便利。NSOperation对象也有 “线程优先级”(thread priority),这决定了运行此操作的线程处在何种优先级上。用 GCD 也可以实现此功能,然而采用操作队列更为简单,只需设置一个属性。

    5、重用 NSOperation 对象

    统内置了一些NSOperation的子类(比如 NSBlockOperation)供开发者调用,要是不想用这些固有子类的话,那就得自己来创建了。这些类就是普通的Objective-C对象,能够存放任何信息。对象在执行时可以充分利用存在于其中的信息,而且还可以随意调用定义在类中的方法。这就比派发队列中那些简单的块要强大许多。这些 NSOperation 类可以在代码中多次使用,它们符合软件开发中的 “不重复”(Don't Repeat Yourself, DRY)原则。

    11、将函数在主线程执行的方法?

    • GCD
    dispatch_async(dispatch_get_main_queue(), ^{
        //需要执行的方法
    });
    
    • NSOperation
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        //需要执行的方法
    }];
    
    [mainQueue addOperation:operation];
    
    • NSThread
    [self performSelector:@selector(method) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES modes:nil];
    
    [self performSelectorOnMainThread:@selector(method) withObject:nil waitUntilDone:YES];
    
    [[NSThread mainThread] performSelector:@selector(method) withObject:nil];
    
    • RunLoop
    [[NSRunLoop mainRunLoop] performSelector:@selector(method) withObject:nil];
    

    12、GCD实现多个请求都完成之后返回结果

    • 同步堵塞
    • 栅栏函数
    • 调度组
    • 信号量
    • RXSwift中的combinelatest

    具体例子:

    根据若干个URL异步加载多张图片,然后在都下载完成后合成一张整图

    let queue = DispatchQueue.global()
    let group = DispatchGroup()
     
    group.enter()
    queue.async(group: group) {
       print("load image1 finished")
       group.leave()
    }
     
    group.enter()
    queue.async(group: group) {
        print("load image2 finished")
        group.leave()
    }
     
    group.enter()
    queue.async(group: group) {
        print("load image3 finished")
        group.leave()
    }
     
    group.notify(queue: queue) {
        print("load all image finished")
    }
    

    13、线程死锁的四个条件

    产生死锁的四个必要条件

    • 互斥条件

    进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

    • 请求和保持条件

    进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但又对自己获得的资源保持不放

    • 不可剥夺条件

    是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

    • 环路等待条件

    是指进程发生死锁后,必然存在一个进程--资源之间的环形链

    14、GCD执行原理

    GCD底层维护了一个可重用线程池,当线程结束后,系统并不第一时间销毁该线程,而是将其放入线程池中待用,若一段时间后仍未使用,则销毁。串行队列底层的线程池只需要一个线程,因此只提供一个线程用来执行任务。而并发队列需要多个线程来实现并发,所以线程池中维护了多个线程。

    具体来说:

    如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。

    如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。

    这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开5-8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。

    而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。

    15、苹果为什么要废弃dispatch_get_current_queue?

    因为dispatch_get_current_queue容易造成死锁,详情点击该API查看官方注释。

    16、谈谈@synchronized()

    Objective-C支持程序中的多线程。这就意味着两个线程有可能同时修改同一个对象,这将在程序中导致严重的问题。为了避免这种多个线程同时执行同一段代码的情况,Objective-C提供了@synchronized()指令。

    指令@synchronized()通过对一段代码的使用进行加锁。其他试图执行该段代码的线程都会被阻塞,直到加锁线程退出执行该段被保护的代码段,也就是说@synchronized()代码块中的最后一条语句已经被执行完毕的时候。

    指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不同的关键代码段,我们应该分别使用不同的信号量。只有在应用程序编程执行多线程之前就创建好所有需要的互斥信号量对象来避免线程间的竞争才是最安全的。

    @synchronizediOS多线程同步机制中最慢的一个,同时也是最方便的一个。

    • @synchronized的使用
    @implementation ThreadSafeQueue
    {
        NSMutableArray *_elements;
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _elements = [NSMutableArray array];
        }
        return self;
    }
    
    - (void)increment
    {
        @synchronized (self) {
            [_elements addObject:element];
        }
    }
    
    @end
    
    • @synchronized的原理

    简单来说就是@synchronized中传入的object的内存地址,被用作key,通过hash map对应的一个系统维护的递归锁

    有兴趣可以看详情内容

    Runloop

    1、Runloop和线程的关系?

    总的说来,Runloop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上Runloop和线程是紧密相连的,可以这样说Runloop是为了线程而生,没有线程,它就没有存在的必要。

    Runloop是线程的基础架构部分, CocoaCoreFundation 都提供了Runloop对象方便配置和管理线程。每个线程,包括程序的主线程(Main Thread )都有与之相应的Runloop对象。

    Runloop和线程的关系

    1、主线程的Runloop默认是启动的。

    iOS的应用程序里面,程序启动后会有一个如下的main()函数

    int main(int argc, char * argv[]) {
       @autoreleasepool {
           return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
       }
    }
    

    主要是UIApplicationMain()函数,这个方法会为主线程(Main Thread)设置一个NSRunLoop对象,这就说明了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

    2、对其它线程(也可以说子线程)来说,Runloop默认是没有启动的,如果需要更多的线程交互则可以手动配置和启动。

    3、在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的Runloop

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    

    2、以scheduledTimerWithTimeInterval的方式触发timer,在滑动列队表,timer会停止,为什么?怎么解决?

    这个问题也是开发中常见的情况,只要明白了Runloop运行的几大模式,相比非常清楚原因。

    造成这个现象的原因在于滑动页面时,当前线程的Runloop切换了model的模式,导致定时器(timer)暂停。

    Runloop中的mode的主要来指定事件在Runloop中的优先级,具体有以下几种:

    Default(NSDefaultRunLoopMode):默认设置
    Connection(NSConnectionReplyMode):用于处理NSConnection相关事件,开发者一般用不到
    Modal(NSModalPanelRunLoopMode):用于处理modal panels事件
    Event Tracking(NSEventTrackingRunLoopMode):用于处理拖拽和用户交互模式
    Common(NSRunloopCommonModes):模式集合,默认包括Default、Modal 、Event Tracking三大模式,可以处理几乎所有事件
    

    在滑动列表时,Runloopmodel由原来的Default模式切换到了Event Tracking模式,对于定时器(timer)原来运行在Default模式中,现在model被切换了,自然就停止工作了。

    解决方法

    • 方法1:将timer加入到NSRunloopCommonModes中
     [[NSRunLoop currentRunLoop]addTimer:timer 
                                  orMode:NSRunLoopCommonModes];
    
    • 方法2:将timer放到子线程中

    将将timer放到子线程中然后开启线程对应的Runloop,这样可以保证与主线程互不干扰,主线程处理滑动事件,timer继续执行自己的事件

    - (void)viewDidLoad {
        [super viewDidLoad];
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
          _timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                     target:self
                                                   selector:@selector(repeat)
                                                   userInfo:nil
                                                    repeats:true];
          [[NSRunLoop currentRunLoop] run];
        });
    }
    
    - (void)repeat {
        NSLog(@"timer repeat");
    }
    

    3、Runloop内部是如何实现的?

    RunLoop 内部的逻辑大致如下:

    4010043-14631c5e87793b69.png

    其内部代码整理如下:

    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }
     
    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }
     
    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
        
        /// 首先根据modeName找到对应mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
        /// 如果mode里没有source/timer/observer, 直接返回。
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
        
        /// 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        
        /// 内部函数,进入loop
        __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
            
            Boolean sourceHandledThisLoop = NO;
            int retVal = 0;
            do {
     
                /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
                /// 4. RunLoop 触发 Source0 (非port) 回调。
                sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
     
                /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
                if (__Source0DidDispatchPortLastTime) {
                    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                    if (hasMsg) goto handle_msg;
                }
                
                /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
                if (!sourceHandledThisLoop) {
                    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                }
                
                /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// • 一个基于 port 的Source 的事件。
                /// • 一个 Timer 到时间了
                /// • RunLoop 自身的超时时间到了
                /// • 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
                }
     
                /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
                
                /// 收到消息,处理消息。
                handle_msg:
     
                /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
                if (msg_is_timer) {
                    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
                } 
     
                /// 9.2 如果有dispatch到main_queue的block,执行block。
                else if (msg_is_dispatch) {
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                } 
     
                /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                else {
                    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                    if (sourceHandledThisLoop) {
                        mach_msg(reply, MACH_SEND_MSG, reply);
                    }
                }
                
                /// 执行加入到Loop的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
     
                if (sourceHandledThisLoop && stopAfterHandle) {
                    /// 进入loop时参数说处理完事件就返回。
                    retVal = kCFRunLoopRunHandledSource;
                } else if (timeout) {
                    /// 超出传入参数标记的超时时间了
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) {
                    /// 被外部调用者强制停止了
                    retVal = kCFRunLoopRunStopped;
                } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                    /// source/timer/observer一个都没有了
                    retVal = kCFRunLoopRunFinished;
                }
                
                /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
            } while (retVal == 0);
        }
        
        /// 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    }
    

    可以看到,实际上RunLoop就是这样一个函数,其内部是一个do-while循环。当你调用 CFRunLoopRun()时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    深入理解RunLoop

    参考

    CFRunLoop

    相关文章

      网友评论

        本文标题:iOS面试题 - 多线程和Runloop

        本文链接:https://www.haomeiwen.com/subject/qccfrqtx.html