C++ 并发编程实战 第十一章 多线程应用的测试和除错
目录
11.1 与并发相关的错误类型
11.1.1 不必要的阻塞
11.1.2 条件竞争
11.2 定位并发错误的技术
11.2.1 代码审阅——发现潜在的错误
11.2.2 通过测试定位并发相关的错误
11.2.3 可测试性设计
11.2.4 多线程测试技术
11.2.5 构建多线程测试代码
11.2.6 测试多线程代码性能
参考《C++ 并发编程实战》 著:Anthony Williams 译: 吴天明
11.1 与并发相关的错误类型
有一些错误与使用并发直接相关,本章重点关注这些错误。通常,并发相关的错误通常有两大类:
不必要的阻塞
条件竞争
11.1.1 不必要的阻塞
这个主题可以分成以下几个问题:
死锁——如在第3章所见,死锁的情况下,两个线程会互相等待。当线程产生死锁,应该完成的任务就会持续搁置。举个例子来说,一些线程是负责对用户界面操作的线程,在死锁的情况下,用户界面就会无响应。另一些例子中,界面接口会保持响应,不过有些任务就无法完成,比如:查询无结果返回或文档未打印。
活锁——与死锁的情况类似。不同的地方在于线程不是阻塞等待,而是在循环中持续检查,例如:自旋锁。比较严重的情况下,其表现和死锁一样(应用不会做任何处理,停止响应),CPU的使用率还居高不下;因为线程还在循环中被检查,而不是阻塞等待。不太严重的情况下,因为使用随机调度,活锁的问题还是可以解决的。
I/O阻塞或外部输入——当线程被外部输入所阻塞,线程也就不能做其他事情了(即使,等待输入的情况永远不会发生)。因此,被外部输入所阻塞,就会让人不太高兴,因为可能有其他线程正在等待这个线程完成某些任务。
11.1.2 条件竞争
条件竞争经常会产生以下几种类型的错误:
数据竞争——因为未同步访问一块共享内存,将会导致代码产生未定义行为。第5章已经介绍了数据竞争,也了解了C++的内存模型。数据竞争通常发生在错误的使用原子操作上,做同步线程的时候,或没使用互斥量保护共享数据的时候。
破坏不变量——主要表现为悬空指针(因为其他线程已经将要访问的数据删除了),随机存储错误(因为局部更新,导致线程读取了不一样的数据),以及双重释放(比如:当两个线程对同一个队列同时执行pop操作,想要删除同一个关联数据),等等。不变量被破坏可以看作为“基于数据”的问题。当独立线程需要以一定顺序执行某些操作时,错误的同步会导致条件竞争,比如:顺序被破坏。
生命周期问题——虽然这类问题也能归结为破坏了不变量,不过这里将其作为一个单独的类别给出。这里的问题是线程会访问不存在的数据,这可能是因为数据被删除或销毁了,或者转移到其他对象中去了。生命周期问题,通常是在一个线程引用了局部变量,在线程还没有完成前,局部变量的“死期”就已经到了,不过这个问题并不止存在这种情况下。当手动调用join()等待线程完成工作,需要保证异常抛出的时候,join()还会等待其他未完成工作的线程。这是线程中基本异常安全的应用。
11.2 定位并发错误的技术
11.2.1 代码审阅——发现潜在的错误
如果让你的同事来审阅代码,他/她肯定对你的代码不是很熟悉。因此,他/她会从不同的角度来看你的代码,然后指出你没有注意的事情。如果你的同事都没有空,你可以叫朋友,或传到网络上,让网友审阅(注意,别传一些机密代码上去)。实在没有人审阅,不要着急。对于初学者,可以将代码放置一段时间——先去做应用的另外的部分,或是阅读一本书籍,亦或出去溜达溜达。在休息之后,当再集中注意力做某些事情(潜意识会考虑很多问题)。同样,当你做完其他事情,回头再看这段代码,就会有些陌生——你可能会从另一个角度来看你自己以前写的代码。
另一种方式就是自己审阅。可以向别人详细的介绍你所写的功能,可能并不是一个真正的人——可能要对玩具熊或橡皮鸡来进行解释,并且我个人觉得写一些比较详细的注释是非常有益的。在解释过程中,会考虑每一行过后,会发生什么事情,有哪些数据被访问了,等等。问自己关于代码的问题,并且向自己解释这些问题。我觉得这是种非常有效的技巧——通过自问自答,对每个问题认真考虑,这些问题往往都会揭示一些问题,也会有益于任何形式的代码审阅。
审阅多线程代码需要考虑的问题
并发访问时,哪些数据需要保护?
如何确定访问数据受到了保护?
是否会有多个线程同时访问这段代码?
这个线程获取了哪个互斥量?
其他线程可能获取哪些互斥量?
两个线程间的操作是否有依赖关系?如何满足这种关系?
这个线程加载的数据还是合法数据吗?数据是否被其他线程修改过?
当假设其他线程可以对数据进行修改,这将意味着什么?并且,怎么确保这样的事情不会发生?
我最喜欢最后一个问题,因为它让我去考虑线程之间的关系。通过假设一个bug和一行代码相关联,你就可以扮演侦探来追踪bug出现的原因。为了让你自己确定代码里面没有bug,需要考虑代码运行的各种情况。数据被多个互斥量所保护时,这种方式尤其有用,比如:使用线程安全队列(第6章),可以对队头和队尾使用独立的互斥量:就是为了确保在持有一个互斥量的时候,访问是安全的,必须保持有其他互斥量的线程不能同时访问同一元素。还需要特别关注的是,对公共数据的显式处理,使用一个指针或引用的方式来获取数据。
倒数第二个问题也很重要,因为这里很容易产生错误:先释放再获取一个互斥量的前提是,其他线程可能会修改共享数据。虽然很明显,但当互斥锁不是立即可见——可能因为是内部对象——就会不知不觉的掉入陷阱中。第6章已经了解到这种情况是怎么引起条件竞争的,以及如何给细粒度线程安全数据结构带来麻烦的。不过,非线程安全栈将top()和pop()操作分开是有意义的,当多线程并发的访问这个栈,问题会马上出现,因为在两个操作的调用间,内部互斥锁已经被释放,并且另一个线程对栈进行了修改。解决方案就是将两个操作合并,就能用同一个锁来对操作的执行进行保护,就消除了条件竞争的问题。
11.2.2 通过测试定位并发相关的错误
11.2.3 可测试性设计
测试多线程代码很困难,所以需要将其变得简单一些。很重要的一件事就是设计代码时,考虑其的可测试性。可测试的单线程代码设计已经说烂了,而且其中许多建议现在依旧适用。通常,如果代码满足一下几点,就很容易进行测试:
每个函数和类的关系都很清楚。
函数短小精悍。
测试用例可以完全控制被测试代码周边的环境。
执行特定操作的代码应该集中测试,而非分布式测试。
需要在完成编写后,考虑如何进行测试。
以上这些在多线程代码中依旧适用。实际上,我会认为对多线程代码的可测试性要比单线程的更为重要,因为多线程的情况更加复杂。最后一个因素尤为重要:即使不在写完代码后,去写测试用例,这也是一个很好的建议,能让你在写代码之前,想想应该怎么去测试它——用什么作为输入,什么情况看起来会让结果变得糟糕,以及如何激发代码中潜在的问题,等等。
并发代码测试的一种最好的方式:去并发化测试。如果代码在线程间的通讯路径上出现问,就可以让一个已通讯的单线程进行执行,这样会减小问题的难度。在对数据进行访问的应用进行测试时,可以使用单线程的方式进行。这样线程通讯和对特定数据块进行访问时只有一个线程,就达到了更容易测试的目的。
例如,当应用设计为一个多线程状态机时,可以将其分为若干块。将每个逻辑状态分开,就能保证对于每个可能的输入事件、转换或其他操作的结果是正确的;这就是单线程测试的技巧,测试用例提供的输入事件将来自于其他线程。之后,核心状态机和消息路由的代码,就能保证时间能以正确的顺序,正确的传递给可单独测试的线程上,不过对于多并发线程,需要为测试专门设计简单的逻辑状态。
或者,如果将代码分割成多个块(比如:读共享数据/变换数据/更新共享数据),就能使用单线程来测试变换数据的部分。麻烦的多线程测试问题,转换成单线程测试读和更新共享数据,就会简单许多。某些库会用其内部变量存储状态时需要小心,当多线程使用同一库中的函数,这个状态就会被共享。这的确是一个问题,并且问题不会马上出现在访问共享数据的代码中。不过,随着你对这个库的熟悉,就会清楚这样的情况会在什么时候出现。之后,可以适当的加一些保护和同步或使用B计划——让多线程安全并发访问的功能。
11.2.4 多线程测试技术
蛮力测试
代码有问题的时候,就要求蛮力测试一定能看到这个错误。这就意味着代码要运行很多遍,可能会有很多线程在同一时间运行。要是有bug出现,只能线程出现特殊调度的时候;代码运行次数的增加,就意味着bug出现的次数会增多。
当有几次代码测试通过,你可能会对代码的正确性有一些信心。如果连续运行10次都通过,你就会更有信心。如果你运行十亿次都通过了,那么你就会认为这段代码没有问题了。
自信的来源是每次测试的结果。如果你的测试粒度很细,就像测试之前的线程安全队列,那么蛮力测试会让你对这段代码持有高度的自信。另一方面,当测试对象体积较大的时候,调度序列将会很长,即使运行了十亿次测试用例,也不让你对这段代码产生什么信心。
组合仿真测试
名字比较口语化,我需要解释一下这个测试是什么意思:使用一种特殊的软件,用来模拟代码运行的真实情况。你应该知道这种软件,能让一台物理机上运行多个虚拟环境或系统环境,而硬件环境则由监控软件来完成。除了环境是模拟的以外,模拟软件会记录对数据序列访问,上锁,以及对每个线程的原子操作。然后使用C++内存模型的规则,重复的运行,从而识别条件竞争和死锁。
虽然,这种组合测试可以保证所有与系统相关的问题都会被找到,不过过于零碎的程序将会在这种测试中耗费太长时间,因为组合数目和执行的操作数量将会随线程的增多呈指数增长态势。这个测试最好留给需要细粒度测试的代码段,而非整个应用。另一个缺点就是,代码对操作的处理,往往会依赖与模拟软件的可用性。
所以,测试需要在正常情况下,运行很多次,不过这样可能会错过一些问题;也可以在一些特殊情况下运行多次,不过这样更像是为了验证某些问题。
使用专用库对代码进行测试
虽然,这个选择不会像组合仿真的方式提供彻底的检查,不过可以通过特别实现的库(使用同步原语)来发现一些问题,比如:互斥量,锁和条件变量。例如,访问某块公共数据的时候,就要将指定的互斥量上锁。数据被访问后,发现一些互斥量已经上锁,就需要确定相关的互斥量是否被访问线程锁住;如果没有,测试库将报告这个错误。当需要测试库对某块代码进行检查时,可以对相应的共享数据进行标记。
多个互斥量同时被一个线程持有时,测试库也会对锁的序列进行记录。如果其他线程以不同的顺序进行上锁,即使在运行的时候测试用例没有发生死锁,测试库都会将这个行为记录为“有潜在死锁”可能。
测试多线程代码时,另一种库可能会用到,以线程原语实现的库,比如:互斥量和条件变量;当多线程代码在等待,或是被条件变量通过notify_one()提醒的某个线程,测试者可以通过线程,获取到锁。就可以让你来安排一些特殊的情况,以验证代码是否会在这些特定的环境下产生期望的结果。
C++标准库实现中,某些测试工具已经存在于标准库中,没有实现的测试工具,可以基于标准库进行实现。
11.2.5 构建多线程测试代码
在特定时间内,需要安排一系列线程,同时去执行指定的代码段。两个线程的情况,就很容易扩展到多个线程。
首先,需要知道每个测试的不同之处:
环境布置代码,必须首先执行
线程设置代码,需要在每个线程上执行
线程上执行的代码,需要有并发性
并发执行结束后,后续代码需要对代码的状态进行断言检查
了解了各个代码块,就需要保证所有事情按计划进行。一种方式是使用一组使用承诺值来表示是否准备好,然后让std::promise等待(复制)一个std::promise std::shared_future来表示就绪状态。每个线程;主线程会等待每个线程上的承诺值设置后,才按下“开始”键。这就能保证每个线程能够同时开始,并且在准备代码执行完成后,并发代码就可以开始执行了;任何线程的特定设置都需要在设置承诺值前完成。最终,主线程会等待所有线程完成,并且检查其最终状态。还需要格外关心异常,所有线程在准备好的情况下,再按下“开始”键;否则,未准备好的线程就不会运行。
清单11.1 对一个队列并发调用push()和pop()的测试用例
1.void test_concurrent_push_and_pop_on_empty_queue()
2.{
threadsafe_queue<int> q;
3.
// 1
4.
5.std::promise<void> go,push_ready,pop_ready;
6.std::shared_future<void> ready(go.get_future());
// 2
// 3
7.
8.std::future<void> push_done;
9.std::future<int> pop_done;
// 4
10.
11.try
12.{
push_done=std::async(std::launch::async,
13.
// 5
14.[&q,ready,&push_ready]()
15.{
16.push_ready.set_value();
17.ready.wait();
q.push(42);
18.
}
19.
);
20.
pop_done=std::async(std::launch::async,
21.
// 6
22.[&q,ready,&pop_ready]()
23.{
24.pop_ready.set_value();
25.ready.wait();
26.return q.pop();
// 7
}
27.
);
28.
29.push_ready.get_future().wait();
30.pop_ready.get_future().wait();
31.go.set_value();
// 9
33.push_done.get();
// 10
34.assert(pop_done.get()==42);
// 8
32.
assert(q.empty());
35.
36.}
37.catch(...)
38.{
go.set_value();
39.
// 12
throw;
40.
}
41.
42.
// 11
}
11.2.6 测试多线程代码性能
选择以并发的方式开发应用,就是为了能够使用日益增长的处理器数量;通过处理器数量的增加,来提升应用的执行效率。因此,确定性能是否有真正的提高就很重要了(就像其他优化一样)。
并发效率中有个特别的问题——可扩展性——你希望代码能很快的运行24次,或在24芯的机器上对数据进行24(或更多)次处理,或其他等价情况。如8.4.2节中所述,当有重要的代码以单线程方式运行时,就会限制性能的提高。因此,在做测试之前,回顾一下代码的设计结构是很有必要的;这样就能判断,代码在24芯的机器上时,性能会不会提高24倍,或是因为有串行部分的存在,最大的加速比只有3。
对数据访问时,处理器之间会有竞争,会对性能有很大的影响。需要合理的权衡性能和处理器的数量,处理器数量太少,就会等待很久;处理器过多,又会因为竞争的原因等待很久。
因此,在对应的系统上通过不同的配置,检查多线程的性能就很有必要,这样可以得到一张性能图。最起码(如果条件允许)需要在一个单处理器的系统上和一个多处理核芯的系统上进行测试。
相关文章:
C++ 并发编程实战 第十一章 多线程应用的测试和除错
目录 11.1 与并发相关的错误类型 11.1.1 不必要的阻塞 11.1.2 条件竞争 11.2 定位并发错误的技术 11.2.1 代码审阅——发现潜在的错误 11.2.2 通过测试定位并发相关的错误 11.2.3 可测试性设计 11.2.4 多线程测试技术 11.2.5 构建多线程测试代码 11.2.6 测试多线程代…...
Redis实现API访问频率限制
🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…...
BGP服务器租用价格表_腾讯云PK阿里云
BGP云服务器像阿里云和腾讯云均是BGP多线网络,速度更快延迟更低,阿里云BGP服务器2核2G3M带宽优惠价格108元一年起,腾讯云BGP服务器2核2G3M带宽95元一年起,阿腾云分享更多云服务器配置如2核4G、4核8G、8核16G等配置价格表如下&…...
时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解
时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解 目录 时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 SSA-VMD麻雀搜索算法SSA优化VMD变分模态分解 可直接运行 分解效果好…...
【CSS如何实现双飞翼布局】
双飞翼布局是一种基于浮动布局的设计模式,主要用于实现三栏布局。它的主要特点是左右两列是浮动的,中间一列使用margin负值来达到“自适应”的效果。这种布局模式可以避免使用嵌套的div,同时也可以保证页面的语义结构清晰。以下是实现双飞翼布…...
服务注册发现机制
二、注册中心选型 1. zk和eureka的区别 zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。 当主节点crash后,需要进行leader的选举,在这个期间内,zk服务是不可用的(当然…...
【postgresql 基础入门】多表联合查询 join与union 并,交,差等集合操作,两者的区别之处
多表数据联合查询 专栏内容: postgresql内核源码分析手写数据库toadb并发编程 开源贡献: toadb开源库 个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤&#x…...
很可惜,pyinstaller不是万能的
近期活不算少,但是真正新的东西很少,基本都是做些相似的功能,所以有精力想想之前悬而未决的问题,比如前两天写的加快软件启动速度的探索,这几天又想起一个之前没有解决的问题,这个问题之前也在博客写过&…...
0/1背包问题
例题HDU-2602 Problem Description Many years ago , in Teddy’s hometown there was a man who was called “Bone Collector”. This man like to collect varies of bones , such as dog’s , cow’s , also he went to the grave … The bone collector had a big bag wi…...
Redis入门到精通——00数据类型
1、String 1.1、介绍 String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M 1.2、…...
PADS9.5使用记录
目录 一、概述 二、PADS Logic IN4148二极管封装 SOD-123封装 SOD-323封装 SOD-523封装 2N3904 1AM 三极管封装 78L05 7533-1 一、概述 PADS Logic 原理图绘制PADS Layout PCB 封装设计PADS Router 布线 二、PADS Logic …...
Axios post请求出现500错误
笔者在编写前端form表单传后端数据的时候,出现了以下问题 一、问题场景 当我用axios发送post请求的时候,出现了500错误 笔者找了很长时间错误,代码没问题,后端接口也没问题,后来发现问题出在实体类上了 当前端post请…...
【Leetcode】171.Excel 表列序号
一、题目 1、题目描述 给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。 例如: A -> 1 B -> 2 C -> 3 … Z -> 26 AA -> 27 AB -> 28 … 示例1: 输入: columnTitle = "A" 输出: 1示例2: 输入: colu…...
2023湖南省赛游记/题解
省赛拖了大哥们的后腿,感觉随便补个正常一队水平的人,我们一队肯定能AK。只能说自己真的菜,全程帮不上什么忙,还负贡献,真的想笑 B 暴力sg #include <bits/stdc.h> #define ll long long #define ull unsigned…...
海信电视U8KL使用体验:参数卷,画质技术也独有!
每个家庭成员对电视都有不同需求,如何能做到兼顾?看似需求众口难调,其实一台海信电视就能满足所有啦。 海信电视的参数不仅是最卷的,同时画质技术还是国内独有的,能把这样一台优秀的电视搬回家,无论电影、…...
E. Mishap in Club
题目: 样例1: 输入 --输出 1 样例2: 输入 --- 输出 3 思路: 数学贪心模拟思路,由于不知道在俱乐部的人数和在外面的人数,又要尽可能少的人数,那么定义两个变量,一个是里面的人数 i…...
UE4 自带体积云应用
新建空关卡 点击该选项 全部点击一遍 拖进场景...
RTP/RTCP 协议讲解
文章目录 前言一、RTP 协议1、RTP 协议概述2、RTP 工作机制3、RTP 协议的报文结构4、wireshark 抓取 RTP 报文 二、RTCP 协议1、RTCP 协议概述2、RTCP 工作机制3、RTCP 数据报4、wireshark 抓取 RTCP 报文 三、RTSP 和 RTP 的关系四、易混淆概念1、RTP over UDP 和 RTP over RT…...
倒计时15天!百度世界2023抢先看
近日消息,在10月17日即将举办的百度世界2023上,百度创始人、董事长兼首席执行官李彦宏将带来主题演讲,“手把手教你做AI原生应用”。 增设社会报名,有机会获得精美伴手礼 目前,百度世界大会已经开放公众参会报名&…...
Redis 哈希(Hash)数据类型和命令(数据类型 二)
基本概念 Hash是一个键值对的集合,其中每个键都是唯一的。每个键都可以关联多个字段和值,这使得Hash非常适合存储对象或结构化数据。 常用命令 存储、获取、删除:hset、hget、hdel # 添加键为name值为lin hset student name lin # 获取 h…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...
ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
用鸿蒙HarmonyOS5实现中国象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的中国象棋小游戏的实现代码。这个实现使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chinesechess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├──…...
