Python技术迷

熬过了五轮技术面,终于拿下京东40K的offer,却倒在了背调环节,京东的背调,从来不只是走个过场。

刚看到个贴子,说有网友熬过五轮技术面,好不容易拿到京东40K的offer,结果卡在背调,直接被撤。 网友们的回复我看了看,有骂京东背调太狠的,有怀疑是前东家乱说话的,也有人嘲讽“谁让你简历不老实”。

Image

我觉得这事吧,关键还是“职业信用”。背调不是走过场,人家花那么多钱招你,肯定要把过去翻一翻:离职原因、绩效、有没有乱摊事,迟早都会露出来。你不能一边想拿大厂高薪,一边对过去一笔勾销。

不过话说回来,有些公司和同事在背调里补刀、添油加醋,这种确实恶心,事实就说事实,别拿大伙前途出气。

从我的角度看,打工人能做的,就是别造假、别编故事,该交代的提前想好,好聚好散。offer会飞,但把职业信用护住了,路没断,下一个机会总轮得到你。

算法题:按序打印

那天晚上快十一点,我在公司楼下蹲着喝奶茶,我们组那个小李突然冒一句:哥,Python 多线程那个“按序打印”你咋写的?我愣了一下,说你先等我喝一口,别一上来就拷打灵魂。

先说下这道题到底在干嘛

题目意思其实特别日常:有三个线程,分别负责打印:

  • 第一个线程:打印 first
  • 第二个线程:打印 second
  • 第三个线程:打印 third

系统调度是乱序的,也就是说第三个线程很可能先跑、第二个后跑,完全不听你的。 但要求是:无论线程怎么启动、怎么抢占 CPU,输出必须是:

firstsecondthird

顺序不能乱。

你看着是不是很像现实里的场景:下单 → 支付 → 发货,这三步顺序不能乱,对吧。

先说一个“错得很有代表性”的写法

很多人一上来会这么整,伪代码大概是这样:

from threading import Thread

deffirst():
    print("first", end='')

defsecond():
    print("second", end='')

defthird():
    print("third", end='')

t1 = Thread(target=first)
t2 = Thread(target=second)
t3 = Thread(target=third)

t1.start()
t2.start()
t3.start()

然后心里想:我明明先 start 了 t1,再 t2 再 t3,应该也会按顺序吧…… 结果一跑:secondfirstthird、thirdfirstsecond,啥都有,就是不给你老实听话。

这里有个坑:start() 只是“把线程扔给系统排队”,什么时候真正执行,不归你管,完全是操作系统调度器说了算,跟你代码里的顺序没啥关系。GIL 也救不了你,它只保证同一时刻只有一个 Python 字节码在执行,不保证线程之间的执行顺序。

正确思路:用“信号”把三个线程串起来

现实世界里面,靠什么保证顺序?红绿灯、排队号、通知短信,本质上都是:

前一个动作完成后,给后一个人发一个“可以开始了”的信号。

在 Python 里,我们可以用两种常见的小玩意儿当“信号”:

  • threading.Semaphore(信号量)
  • threading.Event(事件)

这俩都是“一个线程等待,另一个线程通知”的模型,特别适合这种按序打印。

我先用信号量写一个完整的版本,一看就懂。

用 Semaphore 写一个经典解法

先看代码,类名就叫 Foo,跟题目里一样:

from threading import Semaphore, Thread

classFoo:
def__init__(self):
# second_sem 一开始是 0,表示 second 线程暂时不能过
# third_sem 也是 0,表示 third 线程也不能过
        self.second_sem = Semaphore(0)
        self.third_sem = Semaphore(0)

deffirst(self, printFirst):
# printFirst 是一个函数,内部帮你打印 "first"
        printFirst()
# 告诉 second:我干完了,你可以开始了
        self.second_sem.release()

defsecond(self, printSecond):
# 卡在这里等 first 释放信号
        self.second_sem.acquire()
        printSecond()
# 告诉 third:轮到你了
        self.third_sem.release()

defthird(self, printThird):
# 卡在这里等 second 释放信号
        self.third_sem.acquire()
        printThird()


# 下面模拟三个线程乱启动
defprintFirst():
    print("first", end='')

defprintSecond():
    print("second", end='')

defprintThird():
    print("third", end='')

foo = Foo()

t1 = Thread(target=foo.first, args=(printFirst,))
t2 = Thread(target=foo.second, args=(printSecond,))
t3 = Thread(target=foo.third, args=(printThird,))

# 故意乱序启动,看结果是不是还是 firstsecondthird
t3.start()
t2.start()
t1.start()

t1.join()
t2.join()
t3.join()

你可以脑补一下执行过程:

  • 一开始 second_sem = 0,third_sem = 0
  • 不管 second、third 线程多早启动,都会卡死在 acquire() 那一行,拿不到信号就不能继续
  • 只有当 first() 执行完,调用 second_sem.release(),second 才能从 acquire() 那里醒过来
  • second() 打印完,放行 third_sem
  • 最后 third() 才能执行

所以输出顺序一定是 firstsecondthird,再怎么乱调度也乱不了。

这个写法的好处就是非常直观: 谁要等谁,就在谁前面卡一个 acquire(),前面的人干完活就 release() 一下。

换个写法:用 Event 也能搞定

如果你更喜欢“开关”风格的,可以用 threading.Event,一样简单:

from threading import Event, Thread

classFoo2:
def__init__(self):
        self.first_done = Event()
        self.second_done = Event()

deffirst(self, printFirst):
        printFirst()
# 标记:第一步完成
        self.first_done.set()

defsecond(self, printSecond):
# 等第一步完成
        self.first_done.wait()
        printSecond()
# 标记:第二步完成
        self.second_done.set()

defthird(self, printThird):
# 等第二步完成
        self.second_done.wait()
        printThird()

Event 的语义就是:

  • wait():如果开关没打开,就在这里等
  • set():把开关打开,所有在这里等的人都被放行

逻辑跟信号量版是一样的,只是写法更像“标志位”。

这个套路在哪些地方能用?

别觉得这题只是刷题网站上的玩具,工作里太多地方能套这个模型了,比如:

  • 处理日志时一定要先写本地文件,再异步丢到 MQ,再标记数据库状态
  • 订单流程里,创建订单 → 锁库存 → 发消息,这三步的顺序是刚性的
  • 启动一个复杂服务时,必须等配置加载完、连接池建好,才能对外提供接口

这些场景的核心其实就是:

步骤之间有依赖关系,但执行主体可能是不同线程、不同协程,甚至不同服务。

那思路基本一样: 要么用“信号量/事件”把步骤串起来,要么用队列、MQ 之类的做更粗粒度的“按序”。

行了,按序打印就说到这,我先去给自己续一杯咖啡,有空你可以把上面的代码自己敲一遍,顺便试试故意把线程启动顺序打乱,看输出还能不能稳住。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB