Notes

线程保活

Demo

背景



实际上,上面的跨线程操作能够顺利进行,一个重要的原因是:主线程全局只有一个

回想一下,当我们需要将 UI 操作回传给主线程是怎么操作的呢?

DispatchQueue.main.async {
    // do something related to UI
}

实际上,我们并不是直接把 UI 操作给到主线程,而是给到了队列 main, 通过 main 队列将任务派发给主线程。所以,我们并不是直接面向线程进行操作,直接面向的是队列。而队列的背后,实际上是由若干条线程,等着任务

当我下意识将 Results 交给 main 队列时,Realm 从来就没有报错,根据 Results 不能跨线程的特点,就只有一个可能,main 队列所设计的线程,就只有一条(否则,当主队列派发 Results 相关任务时,必定会出现跨线程错误)

综上


因此,将查询与查找两个任务放在同一个线程中进行确定下来了。接下来,还需要明确一下线程的工作方式

由于线程一次性的特点,所以当用户的输入发生改变时,进行搜索和匹配任务前,都要重新创建一个线程, 是非常浪费资源的。所以,我们需要重用线程,至少在用户还留在搜索界面上时,用的都是同一个线程

在 iOS 上,重用线程的操作,就需要依靠线程保活,即线程可以持续接受并执行任务

如何实现线程保活

线程保活,需要知道线程的一个基础设施,即 RunLoop

简单来说,RunLoop 就是维持线程状态不转为 finished 的工具。RunLoop 在它活着的期间,会一直以低耗能地进行死循环(相对于使用 while true 的高效能死循环),当接受到任何消息时,它就会起来工作了

RunLoop 是需要我们自己创建的,而创建 RunLoop 实例,需要一个 port, 可以想象成,这个 port 就像一个钩子,把 RunLoop 实例与应用拉上了关系

最后,我们将新创建的 RunLoop 与自己管理的线程拉上关系,从此,就实现了线程保活

线程保活的三板斧:

实现线程保活

我们需要声明 3 个属性

private var aliveThreadRunloop: RunLoop? // 保活用的 RunLoop
private var aliveThreadRunloopPort: NSMachPort? // 挂靠 RunLoop 的 port
private var aliveThread: Thread? // 需要活着的线程

创建线程

let t = Thread(target: self, selector: #selector(asyncRun), object: nil)
t.name = "alive"
t.start()
aliveThread = t

保活

@objc private func asyncRun() {
    autoreleasepool { () -> Void in
        guard let aliveThread = aliveThread else { return }
        
        print("hello, can you here me, \(Thread.current.name ?? "unknown") thread")
        
        aliveThreadRunloop = RunLoop.current // 1
        
        aliveThreadRunloopPort = NSMachPort() // 2
        aliveThreadRunloop?.add(aliveThreadRunloopPort!, forMode: .common)
        
        shouldKeepRunning = true // 3
        while shouldKeepRunning && aliveThreadRunloop!.run(mode: .default, before: Date.distantFuture) {} // 4
        
        // ---- 分界线 ----
        
        if Thread.current != aliveThread {
            fatalError("Current thread is \(String(describing: aliveThread.name))")
        }
        
        // 6
        
        // 移除 port
        if let runloopPort = aliveThreadRunloopPort {
            aliveThreadRunloop?.remove(runloopPort, forMode: .common)
        }
        
        // 移除 RunLoop
        if let runloop = aliveThreadRunloop {
            CFRunLoopStop(runloop.getCFRunLoop())
        }
        
        // 停止线程运作
        if !aliveThread.isCancelled {
            aliveThread.cancel()
        }
    }
}
  1. 将创建一个 RunLoop 实例,我觉得 current 是不是命名出现问题了?名字给人的感觉好像就是返回现有的 RunLoop. 实际上不是,这会为当前线程创建一个 RunLoop (如果没有),文档的 API 说明:

     If a run loop does not yet exist for the thread, one is created and returned.
    
    • 同时要注意一点是,这个方法的执行是异步的,所以在运行这个方法时,其所在的线程一定是 alive 这个线程
  2. 创建一个 port, 并将其添加到新创建的 RunLoop 实例中,并指定 Mode, common 覆盖了开发者可以接触到的,经常会切换到的 mode
    • 处理使用 port 挂靠 RunLoop, 还可以使用 timer
  3. 我们将会创建一个短期内可存活的线程,这个 shouldKeepRunning 作为标记,表明了线程是否可以存活
  4. 启动新创建的 RunLoop 实例,到这里,就会发生上面所说的低耗能死循环,而「分界线」下面的代码就暂时不会运行
    • 启动 RunLoop 有多个方法
      • 最简单的 func run(), 但以这种方式启动的 RunLoop, 保证可以永久运行,但不保证可以停止运行

          Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. 
                    
          If you want the run loop to terminate, you shouldn't use this method
        
      • func run(until limitDate: Date), 这种方式启动的 RunLoop, 可以保证一直运行,直到超时时间到了。同样地,以这种方式启动的 RunLoop, 也不保证可以手动停止运行

          Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit.
        
      • func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool, 这种方式启动的 RunLoop, 可以同时制定 mode, 但同样地,保证可以手动停止运行

          Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. 
        
  5. 停止 RunLoop, 即线程可以结束了,我们需要将 shouldKeepRunning 设置为 false, 要注意的是,设置的时候最好是要在同一个线程中设置,防止多个线程同时读写一个变量

     @IBAction func handleStopAliveThread(_ sender: Any) {
         guard let aliveThread = aliveThread else { return }
         perform(#selector(asyncStop), on: aliveThread, with: nil, waitUntilDone: false)
     }
        
     @objc private func asyncStop() {
         shouldKeepRunning = false
     }
    
  6. shouldKeepRunningfalse 时,此时就会执行到了「分界线」后面的代码,此处我们将线程取消,并将之前创建的 RunLoop 对象中的 port 移除,并停止 RunLoop 运转

Mark