微软面试题:100个囚徒1个灯泡,没有任何通信,怎么确定所有人都来过?
2026/7/31 4:13:10 网站建设 项目流程

上周有个朋友去面微软,被问到了一道经典的智力题。

那道题有名有姓,叫 "100 Prisoners in Solitary Cells",在微软官方面试题列表里排得上前十。前面刚聊完 Paxos 和分布式共识,面试官话锋一转,说给你出道智力题。

题目是这样的。

100个囚徒,关在100个单人牢房里,互相之间不能通信。

监狱里有一个特殊的房间,里面只有一个灯泡,一个开关。

每天,狱卒会随机选一个囚徒去这个房间。囚徒进去之后,可以开灯,也可以关灯,也可以什么都不做。

囚徒在被关进牢房之前,可以聚在一起商量一次策略。之后,再也不能见面,不能传纸条,不能敲墙,什么都干不了。

只有一种情况他们可以离开这个房间时做出一个声明:"所有100个囚徒都至少来过这个房间一次。"

如果声明正确,所有人释放。如果错误,所有人处死。

问:你能设计一个策略,确保一定能安全释放?

他当场就说:不可能。

面试官笑了笑:为什么不可能?

他说:没有通信,没有记忆,随机选择,你怎么可能知道谁去过谁没去过?你被选100次,也没法确定别人被选过。

面试官说:你再想想。灯泡是什么?

他愣住了。


为什么这道题看起来不可能

你先停下来想一想,为什么他会觉得不可能。

囚徒面临的核心困境是:

没有任何直接的通信渠道。不能说话,不能写信,不能碰面。你进了房间,出来之后,没有任何方式告诉别人"我来过了"。

没有任何共享记忆。囚徒自己可以记住"我来过几次",但没法记住"别人来过几次"。

选择是完全随机的。同一个囚徒可能被选1次,也可能被选1000次。你不能用"访问次数"来推断"是否所有人都来过"。

所以直觉告诉你:这个问题无解。

但面试官给了提示。

灯泡是什么?


灯泡就是通信

他后来跟我说,面试官这句话点醒了他。

灯泡不是一个装饰品。灯泡是一个共享的、持久化的、可读写的状态。

它只有两个状态:亮(ON)和灭(OFF)。

但这两个状态,就是1个 bit 的信息。

1个 bit 听起来很少。但对于100个互不相通的囚徒来说,这个 bit 就是他们之间唯一的通信桥梁。

上一个囚徒离开房间时灯是亮的,下一个进来的囚徒就能看到。上一个囚徒把灯关了,下一个进来的也能看到。

这不就是 Redis 吗?

Redis 就是一个所有服务共享的内存状态。服务 A 写入一个 key,服务 B 读到这个 key,两个服务就完成了一次通信。它们之间不需要直连,不需要 RPC,共享状态就是通信。

灯泡,就是一个只有1个 key、value 只有 0 或 1 的 Redis。

现在问题变成了:如何用1个 bit 的共享状态,让100个互不相通的节点达成共识?


从最小的 case 开始

好,现在我们知道灯泡是通信渠道了。但1个 bit 怎么用?

老规矩,从最简单的情况开始推。

2个囚徒,1个灯泡。

囚徒 A 和囚徒 B。策略很简单:

  • 指定 A 为"计数者"(Counter)。

  • 囚徒 B 的任务:进房间时,如果灯是灭的,并且自己还没开过灯,就把灯打开。开过一次之后,以后再也不碰开关。

  • 囚徒 A 的任务:进房间时,如果灯是亮的,就把灯关掉,并且计数 +1。

执行过程:

  1. B 进房间,灯是灭的,B 把灯打开。

  2. A 进房间,灯是亮的,A 把灯关掉,计数 = 1。

  3. 计数 = 1 = 2 - 1 = 1,A 宣布:"所有人都来过了。"

完美。2个囚徒,Counter 只需要等1个信号(2个人减去 Counter 自己),计数到1,搞定。

但等一下。如果顺序反过来呢?

  1. A 先进房间,灯是灭的,A 什么都不做,计数 = 0。

  2. B 进房间,灯是灭的,B 把灯打开。

  3. A 再进房间,灯是亮的,A 关灯,计数 = 1。

也没问题。只要 B 开过一次灯,A 迟早会收到这个信号。

3个囚徒,1个灯泡。

指定 A 为计数者。B 和 C 各自只需要开灯一次。

但这里出现了一个关键问题。

假设 B 进房间,灯是灭的,B 把灯打开。然后 C 进房间,灯是亮的。

C 怎么办?

C 还没开过灯,但灯已经是亮的了。C 能不能把灯关掉再打开?不能——因为如果 C 把灯关了,A 进来看到灯是灭的,就会以为"还没人开过灯",C 的信号就丢了。

所以 C 什么都做不了。C 只能等下一次进来时,灯碰巧是灭的,才能开灯。

这就是"单 bit 缓冲区"的局限:缓冲区大小为1,同一时间只能容纳1个信号。如果灯已经是亮的(有1个待处理的信号),后来的囚徒只能等。

但没关系。因为:

  1. B 开了灯。

  2. A 进来,看到灯亮,关灯,计数 = 1。

  3. C 进来,灯是灭的,C 开灯。

  4. A 进来,看到灯亮,关灯,计数 = 2。

  5. 计数 = 2 = 3 - 1 = 2,A 宣布:"所有人都来过了。"

3个囚徒,2个信号,计数到2,搞定。


协议的核心:3条规则

推到这里,你大概已经看到规律了。我们把策略总结成3条规则:

规则1:选出一个计数者。

100个囚徒中,指定1个人作为 Counter,其余99个人是 Signaler。

规则2:Signaler 的行为——只开一次灯。

每次 Signaler 进房间时:

  • 如果灯是灭的(OFF),并且自己还没开过灯,就把灯打开(ON),标记自己"已发送信号"。

  • 如果灯是亮的(ON),什么都不做。

  • 如果自己已经开过灯了,以后再也不碰开关。

规则3:Counter 的行为——关灯 + 计数。

每次 Counter 进房间时:

  • 如果灯是亮的(ON),把灯关掉(OFF),计数 +1。

  • 如果灯是灭的(OFF),什么都不做。

  • 当计数达到 99,Counter 宣布:"所有人都来过了。"

就这么简单。3条规则,一个1 bit 的"Redis",100个互不相通的囚徒,共识达成。


为什么"只开一次灯"是关键

这个协议里最精妙的设计,是 Signaler "只开一次灯"。

为什么不能开多次?

假设囚徒 B 每次进来都开灯。那么 Counter 怎么区分"这是 B 第一次开的灯"还是"B 第10次开的灯"?

没法区分。因为灯只有1个 bit,它不携带"谁开的"和"开了几次"的信息。

如果允许重复开灯,信号就不可控了。Counter 永远不知道自己数到99的时候,到底是99个不同的人开的灯,还是同一个人开了99次。

"只开一次灯"这条规则,把一个不确定的、可能无限重复的信号,变成了一个确定的、恰好发生1次的信号。

用分布式系统的话说:这是一个幂等操作(Idempotent Operation)。

每个 Signaler 的操作是幂等的——不管你调用多少次,效果只有1次。Counter 的计数才是精确的。

这跟支付幂等性的设计一模一样。你设计一个支付接口,同一个订单号不管被调用多少次,只扣款一次。不是靠"聪明的判断"实现的,是靠"幂等设计"保证的。

幂等不是一种能力,是一种约束。你主动约束自己只做一次,系统才能正确计数。


为什么需要一个 Counter

你可能会想:能不能不用 Counter?每个人自己计数行不行?

不行。

因为灯只有1个 bit。如果每个人都去碰开关,这个 bit 就会变成一堆人的写入竞争——你刚关了,他马上又开了,另一个又关了……信息全乱了。

1个 bit 的共享状态,同一时间只能有1个写入者。这就是为什么必须指定一个 Counter。

Counter 是唯一的"消费者"。Signaler 是"生产者"。灯泡是一个大小为1的缓冲队列。

  • 生产者(Signaler)往队列里放消息(开灯)

  • 消费者(Counter)从队列里取消息(关灯 + 计数)

  • 队列满了(灯亮着)的时候,生产者等着

  • 队列空了(灯灭着)的时候,消费者等着

这就是经典的生产者-消费者模式(Producer-Consumer Pattern)

只不过这个队列的容量只有1。

在真实的后端系统里:

  • Kafka 的消费者就是一个 Counter

  • Redis 的 BLPOP 就是消费者从队列里取消息

  • Raft 的 Leader 就是那个被指定的 Counter


完整推演:3个囚徒的完整流程

为了让你完全理解,我把3个囚徒的情况从头到尾推一遍,包括所有可能的情况。

假设灯初始状态为灭(OFF),A 是 Counter,B 和 C 是 Signalers。

理想情况:

访问者

灯状态

动作

计数

B

OFF

开灯(第1次)

0

A

ON

关灯,计数+1

1

C

OFF

开灯(第1次)

1

A

ON

关灯,计数+1

2

-

-

计数=2=(3-1),A宣布!

2

如果 C 先于 B 进来呢?

访问者

灯状态

动作

计数

C

OFF

开灯(第1次)

0

B

ON

什么都不做(灯已亮,等待)

0

A

ON

关灯,计数+1

1

B

OFF

开灯(第1次)

1

A

ON

关灯,计数+1

2

-

-

计数=2=(3-1),A宣布!

2

注意看第2行。B 进来时灯是亮的,B 什么都做不了。但没关系——B 记住了"我还没开过灯"。等 A 把灯关了之后,B 下次进来就能开了。

如果 A 被连续选中很多次呢?

访问者

灯状态

动作

计数

A

OFF

什么都不做

0

A

OFF

什么都不做

0

A

OFF

什么都不做

0

B

OFF

开灯(第1次)

0

A

ON

关灯,计数+1

1

C

OFF

开灯(第1次)

1

A

ON

关灯,计数+1

2

-

-

计数=2=(3-1),A宣布!

2

A 连续进来3次,灯是灭的,什么也做不了。浪费了3次访问。但协议依然正确——只是慢了一点。

这就是分布式系统里说的"最终一致性"(Eventual Consistency):不保证每一步都快,但保证最终一定对。


放大到100个囚徒

逻辑完全一样。从3到100,只是计数目标从2变成99。

100个囚徒:

  • 1个 Counter,计数目标是 99。

  • 99个 Signalers,每人只开灯一次。

  • Counter 每次进房间发现灯亮,关灯 + 计数 +1。

  • 计数到 99,宣布释放。

为什么一定是 99 而不是 100?因为 Counter 自己也是100人之一,他不需要给自己发信号。他只需要确认其余99人都来过。

99 个信号,99 次关灯计数,1 次宣布,100 人释放。

这套协议是绝对正确的。为什么?

因为每个 Signaler 只开灯一次。所以灯被打开的总次数,恰好等于"已经来过且已经发送过信号的 Signaler 数量"。Counter 每次关灯,就代表收到了1个新的信号。当计数到99,意味着99个 Signaler 都至少来过一次。

不存在误判的可能。没有"可能来过"和"确定来过"的模糊地带。99就是99,一个不多,一个不少。


一个更尖锐的问题:要等多久?

协议是正确的,但代价呢?

100个囚徒,每天选1个,Counter 需要收集99个信号。每个信号的传递过程是:

  1. Signaler 进房间发现灯灭,开灯。

  2. Counter 进房间发现灯亮,关灯,计数+1。

问题在于,Counter 被选中的概率只有 1/100。也就是说,Counter 平均每100天才能进一次房间。

99个信号,每个信号平均要等100天让 Counter 来收集,光 Counter 的等待时间就是 99 × 100 = 9900 天。

再加上 Signaler 等待灯灭的时间(早期还有很多人没发信号,等待较短;后期只剩少数人没发信号,等待变长),总体期望时间大约是:

100 × H₉₉ + 100 × 99 ≈ 518 + 9900 ≈ 10418 天

H₉₉ 是第99个调和数,约等于5.18。总期望大约1万天,接近30年

是的,你没看错。用1个 bit 做通信,100个囚徒要等30年才能释放。

但这是期望值。运气好的话可能十几年,运气差的话可能四五十年。关键是:不管多久,协议保证一定能释放。

这就是1个 bit 的代价。

你想要更快的共识?那就得增加通信带宽。2个 bit,速度提升一倍。100个 bit,速度大幅提升。这就是为什么分布式系统需要网络——带宽越大,共识越快。


这道题到底在考什么?

到这里,答案已经清楚了。但面试官真正想看的,不是你能不能背出这个答案。

这道题考的是三层能力

第一层:你能不能看出"灯泡是通信"。

大部分人卡在这一层。觉得"没有通信"就是真的没有通信。但灯泡是一个持久化的共享状态,它就是通信渠道。

在工程世界里,这对应的是:你能不能看出"共享数据库就是通信"、"共享文件系统就是通信"、"消息队列就是通信"。很多开发者天天用 Redis、用 Kafka,但没意识到这些工具的本质就是共享状态,就是节点间的通信桥梁。

第二层:你能不能设计正确的协议。

看出灯泡是通信还不够,你还得设计一个不会出错的协议。"只开一次灯"这个约束,是整个协议的正确性基石。

在工程世界里,这对应的是:你能不能设计幂等接口。你的支付接口能不能保证同一订单不重复扣款?你的消息消费者能不能保证不重复处理?你的分布式锁能不能保证不重复获取?

第三层:你能不能看到系统瓶颈。

协议正确了,但 Counter 是单点瓶颈。99个信号都要经过1个人收集,时间复杂度是 O(n²)。

在工程世界里,这对应的是:你能不能看到"单 Leader 架构的吞吐瓶颈"。Raft 的 Leader 承担所有写入,当节点数量增加,Leader 的压力线性增长。怎么破?分片、多 Leader、无 Leader 架构(Dynamo 风格)……

面试官用一道智力题,把分布式共识的核心问题全考了。


从囚徒到分布式系统:硬核映射

这道题的每一步,都能在真实的分布式系统里找到对应:

1. 灯泡 = 共享存储

灯泡是一个所有囚徒都能访问的、持久化的、1 bit 的共享状态。

在工程世界里:

  • Redis 就是多服务共享的内存状态

  • ZooKeeper 就是分布式协调的共享配置

  • 数据库就是多应用共享的持久化存储

2. 指定 Counter = 领导者选举(Leader Election)

为什么必须指定一个 Counter?因为1个 bit 的状态,只能有1个写入者。多个写入者会互相覆盖。

在工程世界里:

  • Raft 通过超时选举选出 Leader

  • Paxos 通过 Proposer 竞选选出唯一的提案者

  • Kubernetes 通过 Lease 机制确保同一时刻只有一个活跃的控制器

3. "只开一次灯" = 幂等操作(Idempotency)

每个 Signaler 只开灯一次,保证 Counter 的计数是精确的。

在工程世界里:

  • 支付接口用订单号做幂等键,同一订单只扣款一次

  • 消息消费者用消息ID做去重,同一消息只处理一次

  • 分布式锁用 lease token,同一 token 只获取一次锁

4. Counter 关灯+计数 = 消费者消费消息

Counter 是消费者,灯泡是消息队列(容量为1),Signaler 是生产者。

在工程世界里:

  • Kafka 消费者从分区拉取消息,ack 后 offset 前移

  • RabbitMQ 消费者 ack 后消息从队列删除

  • 这道题里,Counter "ack" 的方式就是关灯

5. 计数到99 = 两阶段提交的"全部ACK"(2PC)

Counter 需要99个信号才能宣布,这相当于2PC中协调者需要所有参与者都回复"YES"才能提交。

在工程世界里:

  • 2PC(两阶段提交)的协调者需要所有参与者ACK

  • 分布式事务的Saga模式需要所有补偿操作完成

  • 这道题要求100%确认,不是多数派,是全确认

6. O(n²) 等待时间 = 单Leader吞吐瓶颈

Counter 是唯一的消息消费者,所有信号必须经过它。节点数增加,等待时间平方级增长。

在工程世界里:

  • Raft 单Leader的写入吞吐量受限于Leader的处理能力

  • MySQL主从复制的写入瓶颈在主库

  • 解决方案:分片、多副本并行、增加缓冲区大小


一个容易忽略的细节:灯的初始状态

面试官可能会追问你一个问题:如果灯的初始状态不确定呢?可能亮也可能灭。

这会引入一个微妙的问题:如果灯初始就是亮的,Counter 第一次进来关灯,计数+1,但这个信号不是任何 Signaler 发的——计数虚高了1。

解决方案有几种:

方案1:Counter 第一次进房间时,如果灯是亮的,关掉但不计数。牺牲一次访问来消除初始状态的不确定性。

方案2:每个 Signaler 开灯两次。Counter 计数到 198(99 × 2),这样即使初始灯亮导致多计1次,最终也能达到198。代价是等待时间翻倍。

方案3:在商量策略时约定假设灯初始为灭。如果实际不确定,方案1最稳。

这种细节在工程世界里叫做初始化问题。你的系统启动时,共享状态的初始值是什么?是0还是上次崩溃前的残留值?这直接关系到系统的正确性。

Redis 的 RDB/AOF 持久化、Raft 的日志持久化、ZooKeeper 的 snapshot,都在解决这个问题:怎么确保系统重启后的初始状态是确定的。


面试时的回答策略

如果你在面试中遇到这道题,别急着说"不可能"。

正确的回答路径是这样的:

第一步:确认问题边界。

"囚徒之前能商量一次策略?灯的初始状态是亮还是灭?囚徒能不能记住自己的历史行为?"

确认了边界,你才知道协议设计的约束条件。

第二步:从最小 case 开始推。

"我先考虑2个囚徒的情况。1个 Counter,1个 Signaler。Signaler 开灯一次,Counter 关灯计数。"

从小 case 开始推,面试官就知道你有工程师的思维——先写 base case,再找规律。

第三步:提出完整协议。

"3条规则:选1个 Counter,Signaler 只开灯一次,Counter 关灯计数到 n-1 时宣布。"

第四步:分析正确性。

"为什么一定对?因为每个 Signaler 只开灯一次,所以 Counter 收到的信号数恰好等于已访问的 Signaler 数。不存在误判。"

第五步:分析代价。

"时间复杂度 O(n²),因为 Counter 的访问概率是 1/n,收集 n-1 个信号平均需要 n×(n-1) 次访问。瓶颈在于 Counter 是单点。"

第六步:谈工程映射。

"这个模型对应分布式系统的共识协议:灯泡是共享存储,Counter 是 Leader,幂等操作保证计数精确,单Leader是吞吐瓶颈。"

这六步走下来,面试官就知道你不只是"知道答案",而是真正理解了问题背后的系统设计思想。


这道题最深的一层

最后说一个我自己推完这道题之后的感受。

这道题最反直觉的地方,不是"灯泡是通信"——这个稍微想想就能理解。

最反直觉的是:囚徒不需要聪明。

他们不需要推理、不需要猜测、不需要做任何复杂的判断。Signaler 只需要记住"我开过灯没有",Counter 只需要记住"我数到几了"。每个人只做一件极其简单的事。

但就是这些"不聪明"的个体,通过一个设计良好的协议,达成了100%正确的共识。

这就是分布式系统的核心思想:协议设计大于个体智能。

Paxos 看起来复杂,但每个节点只需要做几件简单的事:提案、承诺、接受。Raft 看起来精巧,但每个节点只需要做三件事:请求投票、追加日志、应用状态机。

不是节点聪明,是协议聪明。

在工程世界里,你设计一个微服务架构,最重要的不是让每个服务变得多智能,而是设计一套协议,让一堆"笨"服务能正确协作:

  • 消息队列保证 at-least-once 传递,消费者负责幂等去重

  • 服务注册中心保证最终一致,客户端负责重试和熔断

  • 分布式锁保证互斥,业务层负责超时和续约

你的系统有多可靠,不取决于单个服务有多强,取决于协议有多好。

100个囚徒用1个灯泡达成了共识,不是因为他们聪明,是因为协议足够好。


回到我那个朋友。

他最后想出了灯泡是通信,但没想出"只开一次灯"的幂等设计。面试官说思路对了,让他回去再想想完整协议。

他最后还是没拿到那个 offer。

但他说出来之后跟我说了一句话,我觉得特别到位:

"以前我觉得分布式系统最难的是算法,现在发现最难的是设计约束。不是你想做什么,而是你约束自己不做什么。"

只开一次灯,就是约束。

不重复扣款,就是约束。

Leader 只有一个,就是约束。

约束不是限制,约束是正确性的保证。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询