码农高天拆解麻将机器人代码 警告多线程并发致鼠标控制冲突

【Code Review】格式,异常处理和多线程的风险 — 码农高天

码农高天

非阻塞子线程若缺乏类级别锁或高阶原子动作封装,多个自动化任务抢占物理鼠标将直接引发状态错乱与程序瘫痪。

AI-assisted summary of 【Code Review】格式,异常处理和多线程的风险 — original source. Check the original for full context.

Source published:

Key ideas

  • 码农高天通过对开源麻将机器人 MahjongCopilot 中百余行中间件代码的 review,指出项目中普遍存在未严格遵守 PEP 8 规范的问题,呼吁开发者借助 Flake8 或 Black 等自动化 Linter 统一代码风格。 (0:00)
  • 代码中重复调用正在运行的任务时仅静默 return,高天指出这属于缺乏防御意识的设计,应根据业务语义明确是返回布尔值、抛出异常还是使用断言阻止系统状态错乱。 (5:00)
  • 多线程驱动单个鼠标操作带来了严重的原子性缺失与竞态条件风险,并发交叉执行或中途被打断(如拖拽中未触发 mouse_up)会导致系统状态彻底崩溃。 (10:00)
  • 高天给出了两种规避并发风险的解决方案:在工程层面对共享鼠标引入类级别线程锁,或在架构层将“拖拽”等复合交互整体提升为不可分割的原子动作。 (10:00)

Detailed notes

## Background 本期视频由博主“码农高天”于 2025 年 4 月 3 日发布在 YouTube 平台上。视频内容是一次针对开源麻将机器人(MahjongCopilot)中鼠标自动化操作中间件的 Code Review,旨在通过一段百余行的 Python 核心代码,剖析其中隐藏的规范与工程细节。

## TL;DR 视频聚焦于自动化任务类 `AutomationTask`,指出代码中存在大量违背 PEP 8 规范的空格排版与不一致的字符串格式化问题,建议引入自动化 Linter 工具予以规范。随后,码农高天分析了命名语义混淆与静默 return 带来的调用端感知缺失,发散探讨了防御性异常处理的设计选择。最后,视频深入剖析了该类在多线程并发场景下的严重安全隐患,揭示了任务交叉抢占、延时破坏上下文以及复合操作中途打断所引发的竞态条件,并给出了引入类级别共享锁与提升原子动作抽象粒度的改进方案。

## Atmosphere & style 整期视频呈现出资深工程师特有的务实、严谨与循循善诱。码农高天语气平实自然,从微观的代码排版细节切入,层层递进到复杂的多线程并发架构,讲解极具逻辑张力。在指出多线程竞态条件等“工程梦魇”时,通过生动的场景推演将抽象的并发 Bug 具象化,带给观众一种在真实排障现场抽丝剥茧般的专业感与启发感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=0s) 码农高天通过对开源麻将机器人 MahjongCopilot 中百余行中间件代码的 review,指出项目中普遍存在未严格遵守 PEP 8 规范的问题,呼吁开发者借助 Flake8 或 Black 等自动化 Linter 统一代码风格。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 代码中重复调用正在运行的任务时仅静默 return,高天指出这属于缺乏防御意识的设计,应根据业务语义明确是返回布尔值、抛出异常还是使用断言阻止系统状态错乱。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 多线程驱动单个鼠标操作带来了严重的原子性缺失与竞态条件风险,并发交叉执行或中途被打断(如拖拽中未触发 mouse_up)会导致系统状态彻底崩溃。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 高天给出了两种规避并发风险的解决方案:在工程层面对共享鼠标引入类级别线程锁,或在架构层将“拖拽”等复合交互整体提升为不可分割的原子动作。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=0s) Code Review 强调代码规模控制,动辄几千行的完整仓库难以在短视频内深入,限定在 100 至 300 行的核心模块最适合细致审查。 - [00:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=0s) AutomationTask 在项目中扮演中间件角色,通过开辟独立线程来执行耗时的连续动作,避免阻塞主程序逻辑。 - [00:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=0s) PEP 8 对类型注解、二元运算符与默认参数的空格有着精细规则,开发者光靠肉眼与记忆极易出现前后矛盾,必须配置自动化 Linter(如 Flake8、Black)保证代码一致性。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 函数参数名为 game_browser 却在内部赋值给 self.executor,无明确映射的命名转换会平白增加阅读者的认知负担。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 使用 dataclass 定义 ActionStep 及其子类是值得肯定的做法,省去了繁琐的手写构造函数,便于通过基类统一操作类型提示。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 任务重复调用时直接隐式 return 会掩盖问题,外部调用方无法感知执行状态;应仔细评估是否该 raise 异常、返回状态布尔值或使用 assert 强校验。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 在同一文件极近的上下文中混用旧式 % 格式化与 f-string 属于不规范编码,新开发的代码应统一采用现代的 f-string 语法。 - [05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 将鼠标按下与弹起之间的延时拆到 Python 线程中 sleep 虽然解放了浏览器主线程,但缺乏注释且暴露了线程安全窗口。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 系统只对应一个物理鼠标,当多个自动化任务在独立线程中并发调度时,极易发生移动与点击操作交错抢占的竞态条件。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 当支持随时被取消或因外部状态变更中途退出时,拖拽等复合动作若被打断,极易遗漏 mouse_up 导致鼠标永久处于按下卡死状态。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 从工程实现角度解决并发冲突,最直接的做法是引入类级别(Class-level)线程锁,强制全局同一时间仅允许一个任务驱动鼠标。 - [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 从架构设计角度解决原子性问题,应重新定义 ActionStep 的粒度,将易受打断的复合行为(如拖拽)自身封装为单次不可拆分的原子操作。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=0s) 项目背景介绍与 PEP 8 代码格式规范

码农高天在视频开篇提到了本次 Code Review 的征集背景。此前在动态里虽然有若干小伙伴报名,但活动明确要求提交一段 100 到 300 行的 Python 代码,因为一期视频的时长有限,主播自己也不可能去深度阅读动辄几千行的大型仓库。不少人直接扔来一个 GitHub 项目地址就让主播去 review,这种过大的项目显然无法处理。最终选中的是一位朋友的代码,也是仅有的几位明确标注了审查区域的提交者之一。该项目是一个相对复杂的麻将机器人(MahjongCopilot),包含了读取信息、算法计算以及底层操作等模块。

本次具体 review 的核心仅有 `game/automation.py` 里的一个类——`AutomationTask`。该类的核心接口是 `start_action_steps`,外部向其传入一系列动作步骤(`action_steps`,如移动鼠标、点击、按下等操作)和一个可选的游戏状态(`game_state`),以此来操控浏览器实现自动化控制。这个类本质上扮演了一个中间件的角色:外部将一系列想要完成的操作交给它,它再去控制浏览器。而它存在的最大意义是维护了一个独立的线程去驱动浏览器,因为一连串操作(如点击后等待几秒再拖动)耗时较长,开发者不希望这些耗时动作阻塞主程序,因此将它们分发到子线程中执行。

```mermaid flowchart LR accTitle: AutomationTask 架构简图 accDescr: 主程序向 AutomationTask 传递动作和状态,AutomationTask 在子线程中调用 GameBrowser 控制浏览器。 subgraph MainProgram["主程序"] Steps["action_steps<br/>(移动/点击等)"] State["game_state<br/>(游戏状态)"] end subgraph Middleware["AutomationTask (中间件)"] Worker["独立线程 Worker"] end subgraph BrowserLayer["浏览器控制"] Executor["GameBrowser (executor)"] end

Steps --> Worker State --> Worker Worker -->|"无阻塞操作"| Executor ```

针对这段代码,码农高天提出的第一个核心建议是引入自动化 Linter 工具。仅仅在短短一个类之中,就存在很多不符合 Python 官方 PEP 8 规范的写法: - **函数参数类型注解**:PEP 8 推荐在参数注解的冒号后面保留一个空格,而冒号前面不加空格;原代码中冒号前后均无空格。 - **参数默认值的等号空格**:当参数带有类型提示(type annotation)时,默认值的赋值等号两边应该各有一个空格;如果参数没有类型提示,等号两边则不加空格。 - **变量类型注解**:与参数规则一致,冒号后面应有空格,赋值等号前后也应保留空格。 - **函数调用的多行参数格式**:原代码用三行传递三个参数,但三行排版格式严重不一致。在函数调用时,关键字参数赋值的等号两端按标准不应加空格。 - **二元运算符(Binary Operation)**:在一般的除法等二元操作中,如果语句仅有一个运算符,其左右两侧都应加空格。 - **多余缩进与空行字符**:在 243 行等逻辑空行上出现了两个缩进(8 个空格),规范的空行上不应包含任何不可见空格字符。

码农高天直言,人类记不住这么多排版细则是很正常的,“记不住是很正常的事情啊,就是因为记不住你才会发现他写的这个程序里面经常有前后不统一的地方”,前面觉得这样写,后面又在 `game_state` 处加上了空格。为了避免这类局部前后矛盾,必须借助 Flake8、Black 或 Pylint 等自动化 Linter 工具来统一代码风格,让项目显得整洁专业。

[05:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=300s) 命名语义、错误状态处理与字符串格式化的一致性

梳理完排版后,码农高天转向具体的功能实现。在类的初始化函数 `__init__` 中传入了三个参数:`game_browser`、`name` 以及 `description`。其中 `game_browser` 是浏览器的抽象层,提供移动鼠标、点击等底层控制,在此处可以被视为一个黑盒。但在赋值时,原作者将 `game_browser` 赋值给了实例属性 `self.executor`。码农高天指出这种命名转换存在歧义:在同一个类中没有必要搞出两套没有明确映射关系的命名体系,“如果你认为它一定是一个 browser,写 browser 就好了对吧;如果你认为他是一个 executor,那这个 argument 就是 executor”。统一命名能显著降低读者的认知负担。此外,类中还初始化了 `_stop_event`、`_thread` 以及记录上次操作时间的变量。

随后代码进入核心入口函数 `start_action_steps`。在动作定义上,原作者定义了一个基础类 `ActionStep` 作为 dataclass,表示一个原子动作(atomic action),并在此基础上派生出鼠标移动、单击、按下、弹起等子类。码农高天肯定了这种做法:使用 dataclass 避免了冗长手写 `__init__` 的模板代码,且让类型提示可以通过基类统合所有原子动作。

进入函数体后,代码首先校验任务是否已经在运行:通过检查线程是否存在且活跃(`is_running`)。如果线程已经在运行,函数直接简单地 `return`。码农高天在此强调,这是一个关键的“设计选择”(design choice): - 如果只是默默地 return,外部调用方根本无法感知分配下去的操作到底是执行了还是没执行。 - 开发者必须思考:这种情况出现是否属于程序异常?是不是需要抛出异常(`raise Exception`)?调用者是否至少需要拿到一个布尔值(`True`/`False`)来确认执行状态? - 如果按正常业务逻辑,该任务在生命周期内只能被调用一次,那么重复调用是否意味着程序状态彻底错乱,进而应该直接使用 `assert` 断言?

这种思维正是面对异常与边界条件时所必需的发散思考。

```mermaid flowchart TD accTitle: start_action_steps 与 Task 执行流程 accDescr: 展示 start_action_steps 的判断分支以及后台 Task 内部的循环检测与 run_step 调用。 Start(["调用 start_action_steps"]) --> CheckRunning{"is_running?"} CheckRunning --"True"--> SilentReturn["直接 return (隐式吞掉调用)"] CheckRunning --"False"--> SpawnThread["启动子线程运行 Task 闭包"]

subgraph TaskLoop["Task 线程内部执行循环"] RecordStep["记录当前 op_step"] --> ForStep["遍历 action_steps"] ForStep --> CheckStop{"_stop_event is set?"} CheckStop --"Yes"--> LogStop["Log 并退出 Task"] CheckStop --"No"--> CheckState{"game_state 改变且未忽略?"} CheckState --"Yes"--> LogAbort["终止并退出 Task"] CheckState --"No"--> RunStep["调用 run_step 执行动作"] RunStep --> ForStep end

SpawnThread --> TaskLoop ```

随后,函数开启一个子线程并执行闭包 `task`。在 `task` 开头,作者读取了代表游戏状态计数器的 `op_step`,但随后定义了一个仅使用了一次的局部变量 `message` 并传给日志输出。码农高天指出这完全可以合并成一行,尤其令人费解的是在极近的代码上下文里,前面的 log 拼接使用的是 `%` 格式化,紧接着下一处 log 却使用了 f-string。码农高天明确指出:“如此近的局部不统一是不应该出现的啊。除非是这段代码已经被维护了 20 年了,在没有 f-string 的时候用的其他的东西……如果你这个代码是最近才开发的,你就统一都用 f-string 就可以了。”

在主循环执行每个 `action_step` 时,代码进行了两层守护: 1. 检查外界是否调用公有方法触发了 `_stop_event`,若触发则记录 log 并直接退出; 2. 若传入了 `game_state`,则检查游戏状态是否发生变化;如果状态发生变更且该操作未标记为忽略状态变更(`ignore_step_change`),说明环境已变,先前的操作序列失效,任务同样直接中断 return。

最后进入 `run_step`,针对不同操作类型调用 `executor` 的动作。这里包含一个细节:在处理单击(`click`)时,作者没有使用底层的 `mouse_click`,而是把点击过程中的 delay 延时操作放到了 Python 线程中,而不是让浏览器自身去延时。码农高天认为此处应该添加注释说明原由,并推测作者可能是为了避免浏览器主线程被 sleep 阻塞导致其它页面动作卡死。

[10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 多线程并发隐患、竞态条件与操作原子性设计

代码虽然行数极少且逻辑直接,但码农高天指出了其中隐藏的最致命架构隐患:**多线程并发下的竞态条件(Race Condition)与原子性(Atomicity)缺失**。

既然任务被放进单独的线程中运行,这意味着系统在理论上可能同时启动多个自动化任务。码农高天用具体场景阐述了并发时的灾难性表现: - **场景一:多任务交叉抢占**。假设 Task A 的逻辑是“将鼠标移到点 A 并单击”,Task B 的逻辑是“将鼠标移到点 B 并单击”。由于线程调度切换,极可能出现 Task A 刚把鼠标移到 A 点,CPU 控制权就被 Task B 抢走;Task B 将鼠标移到了 B 点执行单击,随后控制权交回 Task A,Task A 紧接着在当前位置(即 B 点)补上了一次单击。最终的宏观表现变成了“鼠标先移到 A、再移到 B、在 B 连点两次”,操作顺序彻底崩溃。 - **场景二:拆分 Delay 导致的上下文破坏**。前文提到作者为了解放浏览器线程,把 `mouse_down` 和 `mouse_up` 之间的 delay 放到了 Python 子线程中 `time.sleep`。但在 Python 线程休眠期间,其他线程极有可能趁虚而入,插入移动或其它点击,导致鼠标弹起时的坐标和环境根本不是按下时的状态。 - **场景三:被打断的复合动作**。若外界需要模拟一个“拖拽”效果(`mouse_down` -> 移动 -> `mouse_up`),而任务支持随时被外界 cancel 或由于 `game_state` 改变而中途 return,那么一旦在拖拽途中被打断,`mouse_up` 将永远得不到执行。物理鼠标状态将一直停留在“按下”状态,导致后续所有麻将机器人的逻辑彻底瘫痪。

码农高天总结道:“我提到的这些问题根据应用的不同,它发生的可能性可能会不同……但是理论上你只要有两个 automation task 有可能会同时运行,就会出现这个问题。而这个问题的背后就是多线程的梦魇——原子性问题,就是你究竟需要什么东西是 atomic 的。”

针对这种多线程风险,码农高天给出了两个层次的解决思路: - **工程实现方案(线程锁)**:最直接有效的手段是在任务层加锁。虽然让浏览器线程保持非阻塞,但通过在 `AutomationTask` 类级别引入一个共享锁(Class-level Lock),强制全局同一时刻只能有一个任务在驱动鼠标。“因为我其实很难想象在什么情况下你会需要两个 task 同时工作,你只有一个鼠标嘛,我很难想象两个 task 同时运行是有可能会正确的。” - **架构设计方案(提升原子动作抽象粒度)**:重新审视 `ActionStep` 的定义。如果底层没有锁来进行强力保护,就不能只提供单纯的“按下”、“弹起”这类极小颗粒度动作,而是应该从设计层面上将“拖拽”等不可分割的高级行为本身定义为一个原子动作(Atomic Action),确保其在执行过程中绝对不被中途打断。

在视频的尾声,码农高天表示虽然没有通读整个项目的全部源码,但仅从这百余行代码中就能提炼出这些在工程实践中极易踩坑的关键问题。最后他感谢了 FittingCode 的赞助支持,并提醒观众不要忘记“一键三连”。

## Notable quotes > [10:00](https://www.youtube.com/watch?v=01_4T_UI6q8&t=600s) 你只有一个鼠标嘛,我很难想象两个 task 同时运行是有可能会正确的。 — 码农高天

## Mentioned 码农高天, MahjongCopilot, AutomationTask, ActionStep, PEP 8, Flake8, Black, Pylint, FittingCode

## Video description 这次的code reivew我们看了一个自动化鼠标操作的中间件。很简单的一段代码,但是里面依然有不少你可能不知道的细节。

本次代码:https://github.com/latorc/MahjongCopilot/blob/main/game/automation.py#L192

[Source](https://www.youtube.com/watch?v=01_4T_UI6q8)

中文简报

码农高天拆解麻将机器人代码 警告多线程并发致鼠标控制冲突 非阻塞子线程若缺乏类级别锁或高阶原子动作封装,多个自动化任务抢占物理鼠标将直接引发状态错乱与程序瘫痪。 视频聚焦于自动化任务类 `AutomationTask`,指出代码中存在大量违背 PEP 8 规范的空格排版与不一致的字符串格式化问题,建议引入自动化 Linter 工具予以规范。随后,码农高天分析了命名语义混淆与静默 return 带来的调用端感知缺失,发散探讨了防御性异常处理的设计选择。最后,视频深入剖析了该类在多线程并发场景下的严重安全隐患,揭示了任务交叉抢占、延时破坏上下文以及复合操作中途打断所引发的竞态条件,并给出了引入类级别共享锁与提升原子动作抽象粒度的改进方案。 码农高天通过对开源麻将机器人 MahjongCopilot 中百余行中间件代码的审查,指出项目中普遍存在未严格遵守 PEP 8 规范的问题,呼吁开发者借助 Flake8 或 Black 等自动化 Linter 统一代码风格。 (0:00) 代码中重复调用正在运行的任务时仅静默 return,高天指出这属于缺乏防御意识的设计,应根据业务语义明确是返回布尔值、抛出异常还是使用断言阻止系统状态错乱。 (5:00) 多线程驱动单个鼠标操作带来了严重的原子性缺失与竞态条件风险,并发交叉执行或中途被打断(如拖拽中未触发 mouse_up)会导致系统状态彻底崩溃。 (10:00) 高天给出了两种规避并发风险的解决方案:在工程层面对共享鼠标引入类级别线程锁,或在架构层将“拖拽”等复合交互整体提升为不可分割的原子动作。 (10:00)

Reading summary

码农高天针对麻将机器人项目中的自动化中间件代码展开审查,指出代码在排版一致性、命名规范与静默失败等细节上存在缺陷,并重点强调了多线程并发缺乏原子性保护所带来的严重逻辑隐患。 利用自动化工具规避 PEP 8 规范漏洞 (0:00) 在审查 `AutomationTask` 类时,代码中出现了大量违背 PEP 8 规范的排版问题。具体包括类型注解处冒号前后缺少空格、带类型提示的默认参数等号两侧未保留空格、函数调用中关键字参数赋值格式混乱,以及二元运算符两端缺少空格等问题;此外,部分逻辑空行上还残留了多余缩进字符。码农高天强调,开发者难以单凭记忆维持所有格式细节,代码中频繁出现前后排版不一致正是人为维护的常见现象。因此,项目必须引入 Flake8、Black 或 Pylint 等自动化 Linter 工具统一代码风格,以提升工程规范性。 语义一致性与异常边界设计 (5:00) 在类的实现层面,原代码将初始化传入的 `game_browser` 直接赋值给实例属性 `self.executor`,这种无明确映射的双重命名增加了阅读理解成本,应当在全类中统一术语。此外,代码在极近的上下文中混用了旧式的 `%` 格式化与新的 f-string 语法,暴露出局部编码风格的不统一。在异常处理上,核心入口 `start_action_steps` 发现已有任务在运行时仅简单执行了静默 `return`。码农高天指出这属于欠缺考量的设计选择:静默返回使调用方无法感知任务是否执行,开发者应根据业务契约审视是否应当抛出异常、返回布尔状态,或在生命周期严格受限时使用 `assert` 断言。 多线程竞态条件与操作原子性缺失 (10:00) 由于该中间件使用独立线程驱动浏览器,若外部触发多个自动化任务同时运行,将引发严重的竞态条件与顺序错乱。例如,Task A 执行移至点 A 后,若 CPU 切换至 Task B 移至点 B 并点击,切回 Task A 后补上的点击将直接落于错误位置;若为了避免阻塞浏览器而将延时放在 Python 线程内,休眠期间也极易被其他任务插入操作破坏上下文。此外,对于“按下-移动-弹起”这类复合拖拽操作,一旦由于外部取消或 `game_state` 状态变更中途退出,鼠标将永远停留在物理按下状态,瘫痪后续控制。码农高天提出两种解决思路:一是引入类级别共享锁,强制物理鼠标同一时刻仅由一个任务独占;二是在架构设计上提升原子操作粒度,将不可分割的复合行为抽象为高级原子动作,防止中途被打断。 码农高天总结认为,尽管该模块代码量仅有百余行,但其暴露出多线程编程中关于竞态条件与原子性界定的经典设计盲区;在涉及物理外设模拟时,缺乏排他性锁或高层原子抽象极易导致系统崩溃。

Source quotes

[10:00] 码农高天: 你只有一个鼠标嘛,我很难想象两个 task 同时运行是有可能会正确的。

Original transcript

[0:00] 好我们这期视频还是传统的code review这次code review是fittingcode赞助的那在进行正式的review之前呢我跟大家说一下在这次动态里有若干的小伙伴报了名但是我们说的很明确啊,我们需要的是一个100到300行的排行代码因为一期视频啊它的长度大概只能review这么多包括自己也不可能真的有时间去水平用那么多的代码那些有一些小伙伴呢可能就直接扔过来一个给他地址然后就说你review去吧我打开一看这个项目好几千那这种项目我肯定没办法做了所以最后呢我们选择的是这个朋友的代码他是仅有的几个给出了一块明确区域的人啊我们先简单介绍一下背景啊,他呢基本上就做了一个麻将机器人这个机器人本身还是比较复杂的它有读取信息的部分,有计算的部分,然后有操作的部分他让我帮忙review的啊只有这一个class啊automation task这个class是干嘛的呢它的核心interface就是这个start action steps它通过传入一系列的action steps这些 steps 可能包括移动鼠标点击或者是按下之类的操作和一个可选的game state这个game state就是现在这个游戏里面的各种状态根据这些来操作它的浏览器啊来去真正的实现这个控制所以这个class本身有点像一个中间件就是你把一系列想要完成的操作传给他然后他再去控制那个浏览器完成这些操作那这个中间件存在的最大意义就是它维护了一个线程去控制这个浏览器比如说他这一系列操作可能本身要用好几秒你点一下啊等几秒然后再拖动对吧这些时间可能比较长那这段时间他可能不希望阻塞他的主程序就把这些操作扔到一个县城去进行好那大致介绍完之后我们来看它的代码我给他的第一个建议就是使用一个自动化的Linter我们可以看到在短短的一个class之内它实际上是有很多的写法并不符合Python官方的推荐的比如说这里一个函数的argument的annotation在Pipe8里面官方是推荐这个冒号后面要有一个空格的可以看到吧官方认为这个冒号前后都没有空格是不推荐的那他这里就是没有这个空格包括这个参数的默认值等号前后应该也是有空格的我们看这里啊如果你这个参数是有类型提示的那等号前后就有空格如果你的参数没有类型提示,等号前后没有空格。包括下面这个variable annotation它和参数的规则是一样的我们可以看这里啊冒号后面有空格 等号前后也有空格下面这里有一个函数调用,它是用了三行写三个argument,这个事情本身没有问题。但是我们看他这三行明显长得不一样对吧就他在如此小的一个局部都没有能做到统一这里函书调用的时候正确的做法是等号前后没有空格这里有个除法,它叫做 Binary Operation那在一般情况下如果这个statement只有一个binary operation的时候这个operator的左右也是要有空格的还有一些不易被察觉的点比如说243行这里它是有两个缩近的 也就是八个空格这种逻辑空航上也应该不存在任何字符啊也就是空格那你说这么多细节我怎么可能都记得住呢记不住是很正常的事情啊就是因为记不住你才会发现他写的这个程序里面经常有前后不统一的地方因为前面他觉得是这样的后面呢他又觉得是那样的比如这个game set 对吧 它等号前后又加空格了那正是因为要避免这种前后非常不统一的情况我们才需要用linter我们需要用一个自动化的程序来告诉我们我们的代码哪写的跟官方的推荐是不符合的那用这种Linter呢就会让我们程序显得非常整机啊这是一个好东西我自己是用flake8的当然市面上有很多其他的option对吧你可能用black或者pylint之类的东西但是对于这种相对来说比较大型的项目啊,我强烈建议大家啊,上Linter好我们把格式的部分一气说完了啊我们来看一下功能initialization的时候传进来了三个东西第一个是这个Game BrowserGameBlogger是另外一层的抽象也就是浏览器的抽象抽象完了之后我们可以直接让这个浏览器进行什么鼠标的移动点击之类的这个类呢我们不研究啊把它当做一个黑核nave跟description很简单啊就是传进来的名字啊和这个描述这里他把这个game browser 复制给了executor这个部分的命名我觉得是有一点点小问题的就在同一个class里面你没有必要产生这两种不同的命名他们之间没有明确的联系如果你认为它一定是一个browser [5:00] 写browser就好了对吧如果你认为他是一个executor,那这个argument就是executor就这个名字转换我认为是带来了一点点奇异的啊下面初始化了一些成员啊有一个stop event然后有一个thread还有一个上一次运行action的时间我们从这个入口函数开始读入口函数就是别人调用它的方式就像我刚才说的它传进来的核心内容是这个action steps我们这里看一下它的Action Steps是怎么处理的他首先定义了一个机类action step然后是一个data class每一个action step表示一个atomic action然后他在这个鸡肋的基础上衍生出了一些其他的action比如说移动鼠标对吧或者下面这些啊单击按下谈起那么他这种实现方式有什么好处呢还是有一些的首先使用Data class可以让你的代码变得更加简单你就不用写一个Init函数然后再去复值什么的直接定义成这个样子就可以了然后由于每一个这种操作都有一个共同的鸡肋你在做这种胎片子的时候啊就可以用这个积累来完成大家看看具体是怎么用的 我们等会接着看这里首先判断了一下是不是已经跑起来了如果已经跑起来了直接返回Estrang的定义是这样的如果已经建立了这个thread并且这个threat还存在,你返回true,否则返回false那在这个部分你实际上是有一个design choice对吧就是你要想在什么情况下会出现这个问题因为如果在这个地方你只是简单的return了你的调用者是并不能区分他让你做的这件事你是做了还是没做的我并不是说他这里写的是不对的但是这块起码你应该是思考过的就出现这种情况是不是代表出错了我这里是不是要raising exception或者说我的调用者是不是起码要知道这个东西跑了还是没跑我这里是不是需要return一个true或者false还是说我这个程序在正常运行的情况下这个start action steps只有可能运行一次否则的话一定是我程序写错了这个位置是不是应该是一个assertion这些考虑其实就是我们面对错误情况的一个发散的思维就这个错误他到底属于什么错误我们应该用什么方式来去处理好,在那之后,他直接开了一个Thread然后把这个臂包定义的task扔进去让这个threat开始跑我们来看一下他扔进去的task首先记录一下upstep啊这个upstep相当于这个游戏状态里面的一个技术器游戏只要状态发生改变啊这个step就会变一下这辆行是不是看起来没什么用你为什么要定义message这个message variable就在这用了一次对吧看起来多少点浪费啊这块我感觉写成一行就行了对比一下这三行之后的loggingmessage长度是差不多的对吧为什么一个保存在变量里可以直接写呢当然更重要的是为什么一个用的FString一个用的是%呢就如此近的局部不统一是不应该出现的啊除非是这段代码已经被维护了20年了在没有FString的时候用的其他的东西然后你在incremental implementation的时候你说我想用Fstream这个是可以理解的但如果你这个代码是最近才开发的你就统一都用Fstream就可以了好 接下来是这个部分的核心了对于每一个action step这个action step就是我们刚才说的什么鼠标移动啊什么按键啊什么滚轮啊这些东西如果这里被set了一个stop event这个stop event可以被通过 public method stop给设置就你说停然后就设置一个stop event如果被stop了,log一下,直接返回如果传了这个game state就看一下这个game state是不是发生变化了因为如果这个游戏的状态发生了改变我刚才传的那一系列action step可能就不好使了这里他说如果这个step如果这个操作没有ignore step change那么我就把它停下来也是直接return否则的话往下一点啊这里run step我们看这 run step 的代码其实比较简单对于不同的type,他用这个executor进行不同的操作这里总体来说还是比较好理解的唯一有一个小细节我们看看click这个地方本身这个executor是用mouseclick这个方式的但是在这里他特别写了,我要把delay放到这个python里,而不是这个browser里。那这里为什么他应该是写一个comment的就对于这种从代码上并不能直接读出意义的部分如果你想增加它的可维护性啊是要稍微写下注释的我猜测这里可能是不想让那个浏览器的现场被阻塞就是浏览器那个县城如果你让他去delay的话他其他的事都不干了那你就可能把那个浏览器阻塞很长时间对吧这里呢他把这个Delay [10:00] 到Python里面浏览器就可以正常的工作好那实际上他让我review的代码就是这一小块非常的少对吧包括代码简单也非常直接啊非常易懂那这么小的一块代码我们就没有什么可说的了吗我们回到这里大家还记得这个task是在一个单独的线程里面运行的对吧而它在一个单独的线程里面运行一般情况下意味着有可能有多个Task在同时运行而但凡有多个线程同时运行的情况我们就要去想是不是有可能会产生raising condition那我们想一下如果有两个task同时运行会产生什么效果如果有一个task是把鼠标移到A这个地方然后单击另外一个task是把鼠标移到B这个地方然后单击是不是由于微信的存在这个鼠标可能会先被移到A这个地方然后另外一个县城拿到控制权然后鼠标被移到B那个地方登基然后返回给之前的县城之前那个县城再运行单机于是就变成了鼠标移到A这个地方移到B这个地方,单击再单击这样你的操作是不是就乱套了包括我们刚才提到这个位置为了解放这个浏览器现成他把这个单击的Delay放到了Python这个现场里面运行然而在你python里面sleep的时候是不是有可能有其他的县城来抢占控制权也就是当你的mouse down之后是不是可能会有其他的限程比如说再耽误一下或者是把这个鼠标挪走了导致你这个click在up的时候并没有在正确的点上包括这里外界可以随时cancel你的action如果我的action是先mouse down然后再移动然后再mouse up我来模拟一个脱拽的效果那谁来保证?我在mouth down然后拖之后这个mouse up一定会被运行了如果mout down,然后移动了这个时候我被打断了,我被cancel了我那个mouse up再也不运行了我整个这个机器人还能正常的工作吗我提到的这些问题根据应用的不同,它发生的可能性可能会不同也许在你这个特殊的应用里面这些事情发生的可能性非常低但是理论上你只要有两个automation task有可能会同时运行就会出现这个问题而这个问题的背后就是多线程的梦魇,原子性问题就是你究竟需要什么东西是atomic的比如我刚才提到的拖拽操作这个操作如果不是原子的如果它是可以被打断的就有可能会出现拖一半然后这个Miles起不来的问题解决这个问题有若干的方向最简单的方式我给task加一个锁就我虽然保证我的browser thread 还是自由的但是同时我只允许运行一个Task也就是所有的 Automation Task的Instance可能共享一个class level的lock因为我其实很难想象在什么情况下你会需要两个Task同时工作你只有一个鼠标吗我很难想象两个Task同时运行是有可能会正确的这个是我觉得可以做的解决方案那还有一个其实是design level的就是你前面聊到Action Step,所谓的 Atomic Action他是指应该有最基础的这几个吗如果没有锁来保护他们你是不是有可能需要增加一些原子性操作比如我刚才说的拖拽对吧这个东西中间肯定是不应该被打断的那当然我没有阅读这个项目全部的原代码只是在看这一小段的时候啊这些是我能想到的有可能会发生的问题好那这期视频这里感谢一下非诚扣的赞助我们下个商丹见别忘了一键三零哦