個人同事金司機出的 5 道 iOS 多線程「面試題」

我有一個同事,他既不姓金,也不是司機,但咱們都叫他「金司機」。他跟倉鼠同樣是一個 iOS 工程師,至於叫司機的緣由就不難想到了…… 爲了防止博客被封,在此不舉例子。面試

總之,金司機在這週週會上給組裏同事展現了好幾道他出的「面試題」,成功淘汰了組裏全部同事、甚至包括咱們老大,給平淡的工做帶來了許多歡樂。之因此打引號,是由於這些題只是形式像面試題,其實並不能真的用來面試(並且咱們公司毫不會使用這些題來面試),否則恐怕一我的都招不到了。你們有興趣看看就好,不準噴我同事~swift

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

主線程與主隊列

在看這組題以前,先問本身一個問題:主線程和主隊列的關係是什麼?bash

第一題

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

執行結果是什麼呢?app

第二題

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

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

線程安全

第四題

這個題 Objective-C 和 swift 會有些不同,因此我提供了兩個版本的代碼:ui

Swift:spa

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 是沒問題的,英語也不是大問題。有興趣的朋友歡迎私信倉鼠,我能夠解答關於工做和麪試的各類問題~ 若是是由於這篇文章帶來的推薦獎,我也會所有轉給個人同事金司機,說到作到:)

相關文章
相關標籤/搜索