我的同事金司機出的 5 道 iOS 多線程“面試題”

我有一個同事,他既不姓金,也不是司機,但我們都叫他“金司機”。他跟倉鼠一樣是一個 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] 那個概率低一點兒,但是稍微跑一會兒還是很容易觸發的。

第五題:
上面的代碼直接運行出來是

"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

題解

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

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

后記

本文所有 credit 歸我的同事金司機所有。雖然不是付費文章,但是本文所有的打賞我也會轉給我的同事金司機~ 如果大家覺得這些題有趣的話歡迎打賞哈哈哈~

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,623評論 2 370