本帖最后由 1609089074 于 2019-11-17 15:50 编辑
最近了解到有种叫“线程池”的东西,愁着不知道怎么实际运用,于是艹了一下 obc 的 BukkitScheduler 学习一下,
因为我也比较菜,希望能跟大家交流学习,以下是我个人的理解,如有不对劲的地方非常欢迎指正
一开始我是从 BukkitRunnable 的 runTaskTimerAsynchronously 方法入手,追踪到了 CraftScheduler 的同名方法:
(其实runTaskTimer 和 runTaskTimerAsynchronously 的差别在于方法内 new 的 task 是 CraftTask 还是 CraftAsyncTask )
当从这里(CraftScheduler实例内)提交 BukkitRunnable 任务,此方法将 delay 和 period 规范好后,
生成 task 实例并交给私有方法 handle 处理:
在方法 handle 内,先将 task 定时运行 ,该定时运行的tick时刻为当前tick + 延时tick (此处的 当前tick字段 由方法 mainThreadHeartbeat 进行更新,方法内容先不做展开,下面会再详细讲),
定时设置好后再将 task 加入"提交队列” ,
对于所谓“提交队列”,这里得提到在 CraftTask 内的实例字段volatile @Nullable CraftTask next ,其代指紧跟在该 task 后的 task,可以理解为其以这样的方式代替了一个 ArrayList。所以所谓“加入提交队列”实际上只是将“提交队尾”的 task 实例的字段 next 引用为新的 task ,这其中并没有涉及到任何集合。
到这里,提交任务的流程就走完了。
接下来讲一下调度器的主体运行方法 mainThreadHeartbeat ,
该方法由服务器主线程每tick进行调用(位于nms包MinecraftServer类内的方法,每个服务端版本的混淆名可能都不一样),传入的方法参数 currentTick 即为当前服务器已运行的第几 tick 数。在方法内,先将this.currentTask 更新,载入缓存列表(一般情况下为空列表),然后开始解析提交的 task ,此处调用了私有方法 parsePending,
在方法 parsePending 内,先获取“提交队首”的 task 为局部字段 head(该 head 最开始是调度器实例化时就生成的无实际run()内容的 CraftTask 实例,也是最初的“队尾”变量 ),接下来通过 head.getNext() 遍历提交任务(如果没有新的任务了 就啥也不做),如果任务的周期参数有效 ,任务加入挂起队列 ,并放入任务表(ConcurrentHashMap<Integer, CraftTask>)中,然后清空“提交队列”(将原本是“提交队列”中的 task 的 next 字段设为 null),最后将原本位于“提交队尾”的 task 设为“提交队首”。
解析好“提交队列”后(分配好挂起队列和任务表后),mainThreadHeartbeat 将开始从队首迭代挂起队列的任务,而循环迭代的条件是 [ruby=CraftScheduler#isReady(long)]isReady(long currentTick) ,其内容就是判断:挂起队列不为空并且在队首的 task 的定时运行的时刻点 到了。
如果队首 task 运行时刻到了,mainThreadHeartbeat 将其取出(同时从队列中将其移除,意即 get 并 remove), 若任务周期无效(period值小于 -1)
如果是同步任务 -> 从任务表 移除此任务
调用 parsePending 重新解析“提交队列” // 这里没搞懂再次调用的原因
否则正常进行
如果是同步任务:
try块
task.timings.startTiming(); // org.pigotmc.CustomTimingsHandler.startTiming(); 没搞懂啥用处
直接执行 task.run();
task.timings.stopTiming(); // org.pigotmc.CustomTimingsHandler.stopTiming(); 没搞懂啥用处
catch到异常
打印至log
调用 parsePending 重新解析“提交队列” // 这里也是没搞懂再次调用的原因
否则异步执行:
设置异步debug类 的 next // 不清楚这玩意具体啥用
使用线程池 直接 execute(task)
/* 此线程池字段在调度器初始化时赋值:
* private final Executor executor = Executors .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("Craft Scheduler Thread - %1$d").build());
*/
执行任务完成后准备根据周期进行定时下次执行
如果任务有周期计划(period值大于 0)
定时任务下次执行时刻 ,时刻值为 currentTick + period
将任务加入缓存列表
否则如果是同步任务 -> 从任务表 移除此任务
任务迭代完后,将缓存列表的所有任务放回挂起队列
清空缓存列表
最后调用 this.debugHead = debugHead.getNextHead(currentTick); // 但还是不清楚这玩意具体啥用
到这里,执行任务的流程就走完了。
总结:给自己做的笔记...
1. CraftTask自身有单链表结构(感谢三楼大佬提示这个专业名词),装载刚提交上来还没有挂起的任务。
2. 在调度器核心方法中 运行同步方法是直接调用任务 task.run() ,而运行异步任务也仅是在任务要运行时才调用调度器中唯一的线程池 进行 execute(task);
2.a. 调度器中处理任务队列(同异步的任务都在一个队列中)的核心方法 由服务器主线程调用,因此,无论任务是同步还是异步都由主线程先过手,所以一旦主线程堵塞(或主线程处理出现缓慢)也会影响到异步任务不能及时处理。换个说法来讲,异步线程池的工作仅仅是调用任务 task.run() 而已,这个工作是由主线程叫它做的,它并不会自己去队列里取任务出来处理,所以如果主线程迟缓,还没处理的异步任务一样会被搁置在队列中。
2.b. 实质上,只有 主线程 和 异步线程池 这两个线程在跑任务,而且后者也有可能因为前者出现堵塞、迟缓而导致闲置。这两个线程加起来的吞吐量算是不高的,如果提交的任务量太多很可能会影响运行(用专业名词来讲或许是叫做 tps 过低吧?)。在不涉及bukkit操作的时候根据情况自己再适当开线程池或许能更好的提供吞吐量。
(或许我未写完(记性不太好),待补... )
以下是从 v1_11_R1 反艹出来的 CraftScheduler 代码,仅供学习参考:
以下是从 v1_11_R1 反艹出来的 CraftTask 与 CraftAsyncTask 代码,仅供学习参考:
其实spigot官网上有开源..(感谢二楼大佬的提示)
最近了解到有种叫“线程池”的东西,愁着不知道怎么实际运用,于是艹了一下 obc 的 BukkitScheduler 学习一下,
因为我也比较菜,希望能跟大家交流学习,以下是我个人的理解,如有不对劲的地方非常欢迎指正
一开始我是从 BukkitRunnable 的 runTaskTimerAsynchronously 方法入手,追踪到了 CraftScheduler 的同名方法:
(其实
|
当从这里(CraftScheduler实例内)提交 BukkitRunnable 任务,此方法将 delay 和 period 规范好后,
生成 task 实例并交给私有方法 handle 处理:
|
在方法 handle 内,先
定时设置好后再
|
对于所谓“提交队列”,这里得提到在 CraftTask 内的实例字段
到这里,提交任务的流程就走完了。
接下来讲一下调度器的主体运行方法 mainThreadHeartbeat ,
|
该方法由服务器主线程每tick进行调用(位于nms包MinecraftServer类内的方法,每个服务端版本的混淆名可能都不一样),传入的方法参数 currentTick 即为当前服务器已运行的第几 tick 数。在方法内,先将
|
在方法 parsePending 内,先获取
解析好“提交队列”后(分配好挂起队列和任务表后),mainThreadHeartbeat 将开始从队首迭代挂起队列的任务,而循环迭代的条件是 [ruby=CraftScheduler#isReady(long)]isReady(long currentTick)
|
如果队首 task 运行时刻到了,mainThreadHeartbeat 将其取出(同时从队列中将其移除,意即 get 并 remove), 若任务周期无效(period值小于 -1)
如果是同步任务 -> 从
调用 parsePending 重新解析“提交队列” // 这里没搞懂再次调用的原因
否则正常进行
如果是同步任务:
try块
task.timings.startTiming(); // org.pigotmc.CustomTimingsHandler.startTiming(); 没搞懂啥用处
直接执行 task.run();
task.timings.stopTiming(); // org.pigotmc.CustomTimingsHandler.stopTiming(); 没搞懂啥用处
catch到异常
打印至log
调用 parsePending 重新解析“提交队列” // 这里也是没搞懂再次调用的原因
否则异步执行:
设置
/* 此线程池字段在调度器初始化时赋值:
* private final Executor executor = Executors .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("Craft Scheduler Thread - %1$d").build());
*/
执行任务完成后准备根据周期进行定时下次执行
如果任务有周期计划(period值大于 0)
将任务加入缓存列表
否则如果是同步任务 -> 从
任务迭代完后,将缓存列表的所有任务放回挂起队列
清空缓存列表
最后调用 this.debugHead = debugHead.getNextHead(currentTick); // 但还是不清楚这玩意具体啥用
到这里,执行任务的流程就走完了。
总结:给自己做的笔记...
1. CraftTask自身有单链表结构(感谢三楼大佬提示这个专业名词),装载刚提交上来还没有挂起的任务。
2. 在
2.a. 调度器中处理任务队列(同异步的任务都在一个队列中)的
2.b. 实质上,只有 主线程 和 异步线程池 这两个线程在跑任务,而且后者也有可能因为前者出现堵塞、迟缓而导致闲置。这两个线程加起来的吞吐量算是不高的,如果提交的任务量太多很可能会影响运行(用专业名词来讲或许是叫做 tps 过低吧?)。在不涉及bukkit操作的时候根据情况自己再适当开线程池或许能更好的提供吞吐量。
(或许我未写完(记性不太好),待补... )
以下是从 v1_11_R1 反艹出来的 CraftScheduler 代码,仅供学习参考:
|
以下是从 v1_11_R1 反艹出来的 CraftTask 与 CraftAsyncTask 代码,仅供学习参考:
|
其实spigot官网上有开源..(感谢二楼大佬的提示)
在?链表结构了解一下?
a1294790523 发表于 2019-11-17 10:13
在?链表结构了解一下?
嗷谢谢dalao!涨知识了,原来这种叫做链表啊结果啊,
当时反艹后看了半天才搞清楚这种结构关系,当时就在想,如果是直接有个List应该会更明了吧...
用这样的结构,比起直接用ArrayList,有哪些优势呢?