我的同事金司机出的 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阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345