iOS 多线程面试题

2020 - 05 - 11

Posted by 戴仓薯

主线程与主队列

第一题

let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
  debugPrint("main thread: \(Thread.isMainThread)")
  let value = DispatchQueue.getSpecific(key: key)
  debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().sync(execute: log)
RunLoop.current.run()

执行结果是什么呢?

答:

"main thread: true"
"main queue: false"

看到主线程上也可以运行其他队列。

第二题

let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
  debugPrint("main thread: \(Thread.isMainThread)")
  let value = DispatchQueue.getSpecific(key: key)
  debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().async {
  DispatchQueue.main.async(execute: log)
}
dispatchMain()

什么情况下输出的结果并不是两个 true 呢?

答:

这道题要想出效果比较不容易。所以放一张截图:

看,主队列居然不在主线程上啦!

这里用的这个 API dispatchMain() 如果改成 RunLoop.current.run(),结果就会像我们一般预期的那样是两个 true。而且在 command line 环境下才能出这效果,如果建工程是 iOS app 的话因为有 runloop,所以结果也是两个 true 的。

GCD 与 OperationQueue

第三题

let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { _, activity in
  if activity.contains(.entry) {
    debugPrint("entry")
  } else if activity.contains(.beforeTimers) {
    debugPrint("beforeTimers")
  } else if activity.contains(.beforeSources) {
    debugPrint("beforeSources")
  } else if activity.contains(.beforeWaiting) {
    debugPrint("beforeWaiting")
  } else if activity.contains(.afterWaiting) {
    debugPrint("afterWaiting")
  } else if activity.contains(.exit) {
    debugPrint("exit")
  }
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)

// case 1
DispatchQueue.global().async {
  (0...999).forEach { idx in
    DispatchQueue.main.async {
      debugPrint(idx)
    }
  }
}

// case 2
//DispatchQueue.global().async {
//  let operations = (0...999).map { idx in BlockOperation { debugPrint(idx) } }
//  OperationQueue.main.addOperations(operations, waitUntilFinished: false)
//}

RunLoop.current.run()

上面 GCD 的写法,和被注释掉的 OperationQueue 的写法,print 出来会有什么不同呢?

答:

GCD OperationQueue
"entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" 0 1 2 3 4 ... 996 997 998 999 "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" 0 "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" 1 "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting" 2 "exit" "entry" "beforeTimers" "beforeSources" "beforeWaiting" "afterWaiting"

这个例子可以看出有大量任务派发时用 OperationQueue 比 GCD 要略微不容易造成卡顿一些。

线程安全

第四题

let queue1 = DispatchQueue(label: "queue1")
let queue2 = DispatchQueue(label: "queue2")
var list: [Int] = []

queue1.async {
  while true {
    if list.count < 10 {
      list.append(list.count)
    } else {
      list.removeAll()
    }
  }
}

queue2.async {
  while true {
    // case 1
    list.forEach { debugPrint($0) }

    // case 2
//    let value = list
//    value.forEach { debugPrint($0) }

    // case 3
//    var value = list
//    value.append(100)
  }
}

RunLoop.current.run()

使用 case 1 的代码会 crash 吗?case 2 呢?case 3 呢?

答:

均会 Crash

Runloop

第五题

class Object: NSObject {
    @objc func log() {
        debugPrint("log")
    }
}

var runloop: CFRunLoop!
let semaphore = DispatchSemaphore(value: 0)
let thread = Thread {
    RunLoop.current.add(NSMachPort(), forMode: .common)
    runloop = CFRunLoopGetCurrent()
    semaphore.signal()
    CFRunLoopRun()
}

thread.start()
semaphore.wait()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    CFRunLoopPerformBlock(runloop, CFRunLoopMode.commonModes.rawValue) {
        debugPrint("2")
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        debugPrint("1")
        let object = Object()
        object.log()
//        object.perform(#selector(object.log), on: thread, with: nil, waitUntilDone: false)
//        CFRunLoopWakeUp(runloop)
    }
}

RunLoop.current.run()

这样会输出什么呢?

答:

上面的代码直接运行出来是

"1"
"log"

如果把 object.log() 改成 object.perform(#selector(Object.log), on: thread, with: nil, waitUntilDone: false) 的话就能 print 出来 2 了,就是说 runloop 在 sleep 状态下,performSelector 是可以唤醒 runloop 的,而一次单纯的调用不行。有一个细节就是,如果用CFRunLoopWakeUp(runloop)的话,输出顺序是1 log 2 而用 performSelector 的话顺序是 1 2 log。我的朋友骑神的解释:

perform调用时添加的timer任务会唤醒runloop去处理任务。但因为CFRunLoopPerformBlock的任务更早加入队列中,所以输出优先于log

Table of Contents