我的同事金司機出的 5 道 iOS 多執行緒“面試題”

戴倉薯發表於2018-03-03

我有一個同事,他既不姓金,也不是司機,但我們都叫他“金司機”。他跟倉鼠一樣是一個 iOS 工程師,至於叫司機的原因就不難想到了…… 為了防止部落格被封,在此不舉例子。

總之,金司機在這週週會上給組裡同事展示了好幾道他出的“面試題”,成功淘汰了組裡所有同事、甚至包括我們老大,給平淡的工作帶來了許多歡樂。之所以打引號,是因為這些題只是形式像面試題,其實並不能真的用來面試(而且我們公司絕不會使用這些題來面試),不然恐怕一個人都招不到了。大家有興趣看看就好,不許噴我同事~

程式碼是在 command line 環境下執行的,雖然程式碼是 swift 寫的,不過 API 都是一樣的,寫 Objective-C 的朋友也能一看就懂。我們開始吧~

主執行緒與主佇列

在看這組題之前,先問自己一個問題:主執行緒和主佇列的關係是什麼?

第一題

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()
複製程式碼

執行結果是什麼呢?

第二題

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 呢?

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 出來會有什麼不同呢?

執行緒安全

第四題

這個題 Objective-C 和 swift 會有些不一樣,所以我提供了兩個版本的程式碼:

Swift:

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 呢?

Objective-C:

    dispatch_queue_t queue1 = dispatch_queue_create("queue1", 0);
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", 0);
    
    NSMutableArray* array = [NSMutableArray array];

    dispatch_async(queue1, ^{
      while (true) {
        if (array.count < 10) {
          [array addObject:@(array.count)];
        } else {
          [array removeAllObjects];
        }
      }
    });

    dispatch_async(queue2, ^{
      while (true) {
        // case 1
//        for (NSNumber* number in array) {
//          NSLog(@"%@", number);
//        }

        // case 2
//        NSArray* immutableArray = array;
//        for (NSNumber* number in immutableArray) {
//          NSLog(@"%@", number);
//        }

        // case 3
        NSArray* immutableArray = [array copy];
        for (NSNumber* number in immutableArray) {
          NSLog(@"%@", number);
        }
      }
    });
    [[NSRunLoop currentRunLoop] run];
複製程式碼

使用 case 1 的程式碼會 crash 嗎?case 2 呢?case 3 呢?

Runloop

第五題

class Object: NSObject {
  @objc
  func fun() {
    debugPrint("\(self) fun")
  }
}

var runloop: CFRunLoop!

let sem = DispatchSemaphore(value: 0)

let thread = Thread {
  RunLoop.current.add(NSMachPort(), forMode: .commonModes)

  runloop = CFRunLoopGetCurrent()

  sem.signal()

  CFRunLoopRun()
}

thread.start()

sem.wait()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  CFRunLoopPerformBlock(runloop, CFRunLoopMode.commonModes.rawValue) {
    debugPrint("2")
  }

  DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
    debugPrint("1")
    let object = Object()
    object.fun()
//    CFRunLoopWakeUp(runloop)
  })
}

RunLoop.current.run()
複製程式碼

這樣會輸出什麼呢?

答案

第一題:

"main thread: true"
"main queue: false"
複製程式碼

看到主執行緒上也可以執行其他佇列。

第二題: 這道題要想出效果比較不容易。所以放一張截圖:

Screen Shot 2018-03-03 at 5.11.44 PM.png

看,主佇列居然不在主執行緒上啦!

這裡用的這個 API dispatchMain() 如果改成 RunLoop.current.run(),結果就會像我們一般預期的那樣是兩個 true。而且在 command line 環境下才能出這效果,如果建工程是 iOS app 的話因為有 runloop,所以結果也是兩個 true 的。

第三題: GCD:

"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"
複製程式碼

OperationQueue

"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 要略微不容易造成卡頓一些。

第四題: 這個題其實還挺實用的,答案是兩種語言的每個 case 都會 >< [NSArray copy] 那個概率低一點兒,但是稍微跑一會兒還是很容易觸發的。

感謝樓下評論的朋友,補充一句:Objective-C 的第三個 case 跟前兩個 crash 的原因確實是不一樣的,error message 是 release 一個已經 release 的東西。至於為啥會這樣我也不知道,問題應該在 copy 方法的內部實現裡吧。

第五題: 上面的程式碼直接執行出來是

"1"
"<Runloop.Object: 0x102d05be0> fun"
複製程式碼

如果把 object.fun() 改成 object.perform(#selector(Object.fun), on: thread, with: nil, waitUntilDone: false) 的話就能 print 出來 2 了,就是說 runloop 在 sleep 狀態下,performSelector 是可以喚醒 runloop 的,而一次單純的呼叫不行。

有一個細節就是,如果用CFRunLoopWakeUp(runloop)的話,輸出順序是1 fun 2 而用 performSelector 的話順序是 1 2 fun。我的朋友騎神的解釋:

perform呼叫時新增的timer任務會喚醒runloop去處理任務。但因為CFRunLoopPerformBlock的任務更早加入佇列中,所以輸出優先於fun

題解

倉鼠本來想厚顏無恥地寫一篇付費文章,然後把題解部分作為付費部分,估計肯定賺一波小錢:)但是因為倉鼠比較菜,心虛怕會說錯,所以我就不提供題解啦~ 歡迎大家在評論區討論吧,我也會放出朋友們的解答連結~~

騎神對第一題、第二題的題解

後記

倉鼠公司也在招人。因為以前寫部落格被噴過,至今心有餘悸;所以怕公司被噴,我不敢說是哪個公司了(有這麼招人的嗎?) 總之就是一個外企網際網路公司,座標北京。大部分 swift,很顯然我的同事和老大技術水平都非常強,倉鼠在這是最菜的。而且大家都特別 nice,公司福利待遇也是業內頂尖水平的。我們的面試題非常注重實操,主要都是現場寫程式碼實現小功能,100% 是平常工作最常使用的,絕不會使用上面這些奇奇怪怪的題,大家可以放心。要求的話,現階段只招比較 senior 的人,基本上要求真的有 4 年以上的經驗,有大廠的經歷或者學校背景好的話會比較好~ 不用太擔心對語言的要求,不會 swift 是沒問題的,英語也不是大問題。有興趣的朋友歡迎私信倉鼠,我可以解答關於工作和麵試的各種問題~ 如果是因為這篇文章帶來的推薦獎,我也會全部轉給我的同事金司機,說到做到:)

相關文章