跳转至

窗口管理器 — 一个真 bug 引出的「单线程绘图」纪律

这一章讲什么: 多任务一上,GUI 立刻出事:TaskB 在画自己的窗口, 主任务同时因鼠标事件移动它——两个函数同时改图层管理器的内部状态,屏幕留下残影。 这是数据竞争的第一次正面亮相,解法也由此定型: 窗口的一切绘制和移动,收归主任务一个线程执行;别人想用,发消息请求、等回执。 这条纪律立住后,活动窗口、带标题栏的终端窗口、以及 16 倍的绘制加速,都是顺水推舟。

1. 先看现象:残影 bug 与它的两味解药

主线例子:

  症状:鼠标拖动 TaskB 的窗口 → 留下一道道残影
  机理:TaskB 上下文在跑 layer_manager->Draw()(重画自己的计数窗口),
        鼠标一动,调度器切到主任务跑 layer_manager->MoveRelative()(拖窗口),
        两个方法同时改写 layer_manager 的成员变量和屏幕缓冲 —— 数据竞争
  解法 A:Draw/Move 前后 cli/sti 关中断 —— 简单,但画画是重活,
        关中断太久会错过别的中断(优先级倒置),否决
  解法 B(采用):所有图层操作收归主任务;TaskB 发 kLayer 消息请求,
        主任务画完回一条 kLayerFinish,TaskB 等到回执再继续
  代价:TaskB 的计数速度降到原来的 1/3 —— 「牺牲速度换安全,这是一件好事」

2. 顶层全景

  15.1 残影 bug → kLayer 消息协议(op + layer_id + x/y + src_task);
       TaskB 变成「请求-等回执」结构(主循环同构)
  15.2 活动窗口:ActiveLayer 类(Activate 置顶 + 标题变色;鼠标层永远最高);
       ToplevelWindow 继承 Window(加 title_ 和 InnerWriter);按键按活动窗口分发
  15.3 终端:Terminal 类 + TaskTerminal 任务(0.5s 光标定时器消息);
       std::string 的理由(局部数组指针的悬垂错误);函数重载去重复
  15.4 加速:BlinkCursor 返回重绘区域(7×15 像素)→ DrawArea 消息 →
       实测 240万 → 15万计数,约 16 倍

  图说:先立纪律(单线程绘图),再长功能(活动/终端),最后量化抠性能。

3. 核心原理

3.1 数据竞争与一个架构决定

第 14 章的诚实预告在这章兑现:两个任务并发调 layer_manager 的方法, 「我们无法预测数据冲突时会发生什么」——这次的表现是屏幕残影,下次可能就是内存损坏1。 两条路:cli 包住(简单,但重活期间关中断 = 长临界区,错过中断、低优先级窗口拖累高优先级 中断处理——这就是优先级倒置,任务调度最常见的问题之一)2; 或者把共享状态(图层系统)的访问收归单一执行者。作者选了后者,执行者是主任务 (书中也承认「专用任务更干净」,只是先不创建新任务)3。

协议设计:kLayer 消息带 op(画/移)、layer_id、坐标、src_task(谁的请求); 处理完回一条 kLayerFinish 给 src_task;请求方用一个「主循环同构」的 while 循环等回执—— 等待期间照样处理自己的消息(比如再被塞别的请求)4。 TaskB 因此减速到 1/3(频繁切换 + 主任务开销),作者的评价是「在这种情况下,我们牺牲了 程序的速度来提高安全性,所以这是一件好事」5——架构级的取舍,明码标价。

3.2 活动窗口与 ToplevelWindow

「活动窗口」= 被选中、置顶、键盘输入归它的那个,靠标题栏颜色区分。 新组件 ActiveLayer:Activate(id) 做两件事——把图层置顶(但永远在鼠标层之下,所以要有 SetMouseLayer 单独注册鼠标层),和切换新旧窗口的激活外观6。

外观切换遇到了一个 OOP 教科书问题:Window 类只是「一块绘图区域」,根本不知道标题栏的存在 (控制台、鼠标光标也是 Window)。解法:派生 ToplevelWindow——继承 Window,加 title_ 成员, 并把 Activate/Deactivate 声明为 virtual(父类空实现,子类重写画标题栏; 父类析构函数也必须 virtual)7。 两个细节值得收:覆盖父类方法时先调父类同名方法(空实现也调,防将来父类长出了功能); 以及 InnerWriter——把坐标原点平移到窗口内容区的写手,免得每个画图的地方都记一遍 「左右各 4 像素、顶部 24 像素是边框」8。 按键处理随之按活动窗口分发:文本框活动就进文本框,TaskB 活动就响应 S/W,否则记一行 「key push not handled」9。

3.3 终端成为任务,字符串救了一个悬垂指针

终端的关键架构决定:它是一个独立任务(TaskTerminal),而不是主循环的一个分支—— 「想增加终端数量时,只需要增加任务数量」15。Terminal 类持有窗口、图层和光标状态; 0.5 秒的闪烁定时器超时消息发到终端任务自己的队列,ReceiveMessage 拿不到就 Sleep (第 14 章原语的标准用法)10。

中途有个 C++ 陷阱被认真讲了一遍:窗口标题存在 const char* title_ 里, 构造函数拿局部数组 win_title 去初始化——函数一返回,指针指向已销毁的数组, 重画标题时读到的是垃圾。修法:成员类型换 std::string,构造时把内容拷进堆上 (new/malloc 此时已可用)11。 DrawTextbox 也趁机做了函数重载版(接收颜色),终端(黑底)和文本框(白底)共用一份实现, 「复制粘贴会产生重复的代码」被点名否决12。

3.4 16 倍:把「画多少」也管起来

光标只有 7×15 像素,闪烁却整窗重绘。解法是把第 10 章的范围重绘做成消息: BlinkCursor() 返回要重绘的矩形,终端任务发 DrawArea 请求(op=DrawArea + w/h), 主任务把「窗口区域 ∩ 指定区域」算出来,只画那一小块——注意坐标系换算 (请求方以窗口左上角为基准,图层管理器以屏幕为基准,求交前要把窗口位置加进去)13。 实测:平均约 240 万计数 → 约 15 万计数,约 16 倍14。 「先测量,再优化」的第三次完整执行。

4. 作者的判断与证据

判断:单线程绘图是值得付 3 倍减速的纪律。 数据竞争的替代疗法(关中断)会引入 优先级倒置,而消息化之后,「共享状态」收敛到主任务一处,并发面立刻可推理。

判断:字符串、定时器、任务,这些「底层件」每成熟一个,上层设计就换一版。 本章标题用 std::string、终端用任务+消息,都是前面章节的伏笔兑现。

证据: 残影复现与消除的截图对比;16 倍加速的前后测量;以及「1/3 速度」这个付出去的代价 被如实记账。

5. 边界与局限

  • 主任务是单点:所有绘图请求都排队等它,主任务卡 = 全屏卡;真正的窗口系统会用 合成器(compositor)任务 + 双缓冲更进一步(本书 10.5 的后置缓冲已有一人份雏形)。
  • kLayerFinish 是同步回执:请求方傻等,流水线不起来;管道时代(第 29 章)才会 真正处理「异步回执」。
  • 没有 Alt+Tab:键盘切窗口作者明确留作练习。
  • DrawArea 的重绘区域靠各调用方自觉返回:算错了不会崩,但会花屏——信任式接口。

6. 可带走的

本章主线例子,一行写完: TaskB 画窗撞主任务移窗出残影 → 图层操作全归主任务(kLayer 请求 + kLayerFinish 回执,减速 1/3)→ ActiveLayer 置顶换色 → TaskTerminal 独立任务 → 光标只重绘 7×15 区域(16 倍)。

  1. 数据竞争的形态之一:两个任务并发改同一块 GUI 状态;解法是把共享状态的访问收归单线程;
  2. 长临界区(重活 + 关中断)= 优先级倒置,是调度常见病;
  3. 请求-回执协议:op 枚举 + 目标 id + src_task 回执;等待回执的循环与主循环同构;
  4. 不知道的概念放父类,知道的概念放子类:Window/ToplevelWindow 的分法;
  5. 覆盖虚函数先调父类同名方法;悬垂指针(局部数组的指针存进长寿命对象)用 std::string 根治;
  6. 每任务一个消息队列 + 没消息就睡,是「服务化」的通用骨架;
  7. 局部重绘的量化收益(16 倍)再次印证「先测量,再优化」。

7. 原文地图

主题 原书章 原文位置
残影 bug 与数据竞争 15.1 在主线程中绘制窗口(osbook_day15a) text/99-ch15-01-15-1-osbook-day15a.txt:9(搜「残影」) · text/99-ch15-01-15-1-osbook-day15a.txt:28(搜「数据冲突」)
优先级倒置与两法取舍 15.1 在主线程中绘制窗口(osbook_day15a) text/99-ch15-01-15-1-osbook-day15a.txt:47(搜「优先级倒置」) · text/99-ch15-01-15-1-osbook-day15a.txt:49(搜「第二种方法」)
kLayer 协议 15.1 在主线程中绘制窗口(osbook_day15a) text/99-ch15-01-15-1-osbook-day15a.txt:50(搜「kLayer」) · text/99-ch15-01-15-1-osbook-day15a.txt:73(搜「src_task」)
1/3 速度的取舍 15.1 在主线程中绘制窗口(osbook_day15a) text/99-ch15-01-15-1-osbook-day15a.txt:98(搜「1/3」) · text/99-ch15-01-15-1-osbook-day15a.txt:100(搜「牺牲」)
活动窗口定义 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:7(搜「活动意味着」)
ActiveLayer 与鼠标层 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:13(搜「ActiveLayer类」) · text/100-ch15-02-15-2-osbook-day15b.txt:15(搜「SetMouseLayer」)
Window 不知标题栏 → ToplevelWindow 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:45(搜「标题栏只是在窗口的绘图区域中绘制的一张图片」) · text/100-ch15-02-15-2-osbook-day15b.txt:49(搜「ToplevelWindow」)
虚析构与调父类方法 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:51(搜「虚拟析构函数」) · text/100-ch15-02-15-2-osbook-day15b.txt:102(搜「以防万一」)
InnerWriter 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:84(搜「InnerAreaWriter」)
按键按活动窗口分发 15.2 活动窗口(osbook_day15b) text/100-ch15-02-15-2-osbook-day15b.txt:132(搜「key push not handled」)
终端作为任务 15.3 终端窗口(osbook_day15c) text/101-ch15-03-15-3-osbook-day15c.txt:18(搜「专门的任务」)
std::string 与悬垂指针 15.3 终端窗口(osbook_day15c) text/101-ch15-03-15-3-osbook-day15c.txt:10(搜「已销毁」)
DrawTextbox 重载 15.3 终端窗口(osbook_day15c) text/101-ch15-03-15-3-osbook-day15c.txt:83(搜「重复的代码」) · text/102-ch15-04-15-4-osbook-day15d.txt:3(搜「オーバロード」——日文残页,中文 :88 搜「函数重载」)
DrawArea 与坐标系换算 15.4 加速绘图(osbook_day15d) text/102-ch15-04-15-4-osbook-day15d.txt:23(搜「指定重绘范围」) · text/102-ch15-04-15-4-osbook-day15d.txt:25(搜「坐标系」)
16 倍加速实测 15.4 加速绘图(osbook_day15d) text/102-ch15-04-15-4-osbook-day15d.txt:71(搜「240」) · text/102-ch15-04-15-4-osbook-day15d.txt:90(搜「16倍」)

  1. 出处:「15.1 在主线程中绘制窗口(osbook_day15a)」(text/99-ch15-01-15-1-osbook-day15a.txt:9(搜「残影」); text/99-ch15-01-15-1-osbook-day15a.txt:28(搜「数据冲突」))。 ↩

  2. 出处:「15.1 在主线程中绘制窗口(osbook_day15a)」(text/99-ch15-01-15-1-osbook-day15a.txt:47(搜「优先级倒置」); text/99-ch15-01-15-1-osbook-day15a.txt:49(搜「第二种方法」))。 ↩

  3. 出处:「15.1 在主线程中绘制窗口(osbook_day15a)」(text/99-ch15-01-15-1-osbook-day15a.txt:50(搜「kLayer」); text/99-ch15-01-15-1-osbook-day15a.txt:73(搜「src_task」))。 ↩

  4. 出处:「15.1 在主线程中绘制窗口(osbook_day15a)」(text/99-ch15-01-15-1-osbook-day15a.txt:98(搜「1/3」); text/99-ch15-01-15-1-osbook-day15a.txt:100(搜「牺牲」))。 ↩

  5. 出处:「15.2 活动窗口(osbook_day15b)」(text/100-ch15-02-15-2-osbook-day15b.txt:7(搜「活动意味着」))。 ↩

  6. 出处:「15.2 活动窗口(osbook_day15b)」(text/100-ch15-02-15-2-osbook-day15b.txt:13(搜「ActiveLayer类」); text/100-ch15-02-15-2-osbook-day15b.txt:15(搜「SetMouseLayer」))。 ↩

  7. 出处:「15.2 活动窗口(osbook_day15b)」(text/100-ch15-02-15-2-osbook-day15b.txt:45(搜「标题栏只是在窗口的绘图区域中绘制的一张图片」); text/100-ch15-02-15-2-osbook-day15b.txt:49(搜「ToplevelWindow」))。 ↩

  8. 出处:「15.2 活动窗口(osbook_day15b)」(text/100-ch15-02-15-2-osbook-day15b.txt:51(搜「虚拟析构函数」); text/100-ch15-02-15-2-osbook-day15b.txt:102(搜「以防万一」))。 ↩

  9. 出处:「15.2 活动窗口(osbook_day15b)」(text/100-ch15-02-15-2-osbook-day15b.txt:84(搜「InnerAreaWriter」))。 ↩

  10. 出处:「15.3 终端窗口(osbook_day15c)」(text/101-ch15-03-15-3-osbook-day15c.txt:18(搜「专门的任务」))。 ↩

  11. 出处:「15.3 终端窗口(osbook_day15c)」(text/101-ch15-03-15-3-osbook-day15c.txt:10(搜「已销毁」))。 ↩

  12. 出处:「15.3 终端窗口(osbook_day15c)」(text/101-ch15-03-15-3-osbook-day15c.txt:83(搜「重复的代码」))。 ↩

  13. 出处:「15.4 加速绘图(osbook_day15d)」(text/102-ch15-04-15-4-osbook-day15d.txt:23(搜「指定重绘范围」); text/102-ch15-04-15-4-osbook-day15d.txt:25(搜「坐标系」))。 ↩

  14. 出处:「15.4 加速绘图(osbook_day15d)」(text/102-ch15-04-15-4-osbook-day15d.txt:71(搜「240」); text/102-ch15-04-15-4-osbook-day15d.txt:90(搜「16倍」))。 ↩

  15. 出处:「15.2 活动窗口」(text/100-ch15-02-15-2-osbook-day15b.txt:132,搜「key push not handled」)。 ↩