avz_task:无炮挂机的一种思路
 
Notifications
Clear all

avz_task:无炮挂机的一种思路


维生素B
Posts: 106
Topic starter
(@b-2-2)
复合运算
Joined: 2 years ago

一 目录

一、目录

二、简介

三、使用例

四、实现

二 简介

avz_task是一个基于AvZ 2022 10 01版本的拓展,由一个头文件avz_task.h组成,它提供了一套基于阻塞与并发的task时间管理体系。

首先介绍API。值得注意的是,本文件中所有函数都在namespace aex下。

既然叫avz_task,核心函数肯定就是CreateTask喽……传入参数为一个函数,返回值为一个对象,用来指代创建的task。具体用途,就是创建一个task来执行给定的函数。值得注意的是,在执行函数前禁止了AvZ插入(执行完毕后会智能恢复)。

既然都需要task执行了,函数肯定不一样~一下子就能执行完的函数就不用CreateTask了。因此第二个核心函数就是WaitOneFrame,以及它的衍生WaitFor和WaitTill。WaitOneFrame只能在task内执行,将会阻塞这个task,但是不会影响其它task。WaitFor和WaitTill的接口类似于Delay和SetTime,内部相当于重复地调用WaitOneFrame。

然后还有一些其它函数,比如KillTask,传入与CreateTask的返回值相同类型的对象,用途就是杀死传入的对象指代的task。(呜呜呜好残忍)

可以看出,task之间是并发的,但是单个task内部采取阻塞语义,而我们知道阻塞语义比插入那一套更符合人类直觉且更能了解运行时情况(当然也比巨大的TickRunner符合人类直觉且易于编写),因此task整体相对OperationQueue那一套是有优势的,尤其适用于情况灵活多变的无炮挂机。当然,这种模式也需要用户注意变量生命周期等,但总体来看利大于弊。

当然,任何情况下都可以按需灵活选择时间管理方式,比如Reisen的AvZDSL也广泛适用于多种情况,且编写和阅读都非常简洁。

三 使用例

先放图。

图片

可以看到,本来各项压力几乎为零的减速无还被加了一炮,可见作者无炮技术太差。

好吧加一炮就是为了脚本写起来方便。重点不在阵型,在于脚本,这里简单分析一下核心部分。

图片

首先是一堆函数定义,没啥好说的。SafeCardInTask是根据avz_more.h中的SafeCard改编的适配task的函数。

图片

w1, w10起手。可以看到,这里直接进行的用卡操作很少,大部分由Wait和设置枚举变量next组成。这个next在下面起作用。

图片

可以看到,主循环非常简单,却实现了以C2为主体的I-N-I'-PA卡序,这就体现了task的阻塞式语义的优势。哦对了,这里的nowwave是定义在很外面的一个在每波刷新时更新的整数值。(使用state的想法由Reisen提出,本来这里是使用goto的)

图片

收尾就体现出一个长,和主循环形成鲜明对比……主要是因为在w9/w19刷新时kill了主循环所在的task,导致一堆烂摊子。可以看到,Ash::operator()里定义了self而非直接使用this,主要是考虑到*this的生命周期极短。

主要内容就这么多,其它还有垫材三叶草倭瓜简单关节奏等等,由于写得惨不忍睹就不在这里细说了。可以看出,节奏简单的波动无炮都可以套这个起手—主循环—收尾的模板。期待ME核聚变用task写成,我猜不会太难。

四 实现

本来主要内容已经完了……直到这个作者闲的没事干想写写实现。由于代码过多就不放图了,自行体会。文字很乱。

那么从文件顺序说起。首先是一个我也不知道为什么要叫coroutine的namespace coroutine,提供了将并行的多线程封装成并发的协程的一套函数(当然封装得不是很好……)。主要实现是管理一个stack,以栈顶元素表示当前运行的线程,并提供MoveDown和YieldUp函数来push/pop stack并阻塞当前线程直到栈顶元素再次符合当前线程。同时,也有CreateThread函数创建符合协程要求、规律的线程,为了符合这里的基本要求,线程在执行函数前是阻塞的。同时,在执行完毕后会把控制权上交上一级thread,也就是pop stack。当然,在一切这些操作前都要先start协程,这个函数就是初始化栈让栈顶有元素。然后就是重量级的exit函数,它会清空栈并设置exit flag,然后那些正在阻塞的线程检测到exit flag就纷纷抛出异常,没错是异常。当然,这个异常不是为了杀死程序……这个异常会在thread运行的函数体的最外层被catch,然后直接退出函数的执行。(高级goto)

然后就是更加重量级的初始化及结束协程的代码,这里是放在了TickTasks__函数中,它是管理thread list的函数。前面提到了coroutine::Start函数,这个函数坑爹就坑爹在需要传入一个thread……于是TickTasks__就定义了一个主thread并让这个thread每帧MoveDown thread list里的thread,并检测是否删除某些thread(当然没有轻易调用析构函数)。当然,这个“每帧”是通过外面的tickrunner与这个thread互相设置一个atomic<bool>实现的。然后就是注册了AvZ::ScriptExitDeal,来调用coroutine::Exit并join那些纷纷因为catch异常而结束的threads(当然也要终结上文提到的主thread),然后重设那些全局变量以让多次运行结果正确。(这里就可以看出来coroutine其实没完美封装thread,这个函数该atomic照样atomic,该mutex照样mutex)

然后就可以和thread说拜拜了……上面提到的WaitOneFrame实际就是调用了coroutine::YieldUp来让执行权重返主线程。CreateTask也是coroutine::CreateThread套了层皮,额外执行一些管理thread list的东西。哦对了,还有个insert::InsertGuardPlus,也是为了适配CreateTask需求的多个InsertGuard同时存在而定义的类,它在多个InsertGuard同时存在时也有正常的表现。

最后放上一张图,是使用luastg复刻纯符「纯粹的弹幕地狱」的代码的一部分(刻得很烂,别在意)。luastg的task系统就是avz_task的灵感来源。

图片
4 Replies
维生素B
Posts: 106
Topic starter
(@b-2-2)
复合运算
Joined: 2 years ago

我佛,没贴下载链接。懒得搞了,有兴趣的从AvZ群下,最新的avz_task.zip就包含avz_task.h和减速一炮挂机脚本

Reply
大帅曾是个蒟蒻
Posts: 7
(@dashzeng)
冰消珊瑚
Joined: 2 years ago

好的

Reply
全麦
Posts: 26
(@qrmd)
小丑压制
Joined: 2 years ago

挺好的思路,关键是多个task之间的阻塞互不影响。

Reply
1 Reply
全麦
(@qrmd)
Joined: 2 years ago

小丑压制
Posts: 26

更重要的是可读性比InsertOperation套娃或者递归好多了。

Reply
Scroll to Top
en_USEnglish
Powered by TranslatePress »