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