jarlyyn
发表于 2023-6-6 13:42:18
newstart 发表于 2023-6-6 01:32 PM
还好吧
做好每一个单独的Command模块就可以了
反正目前运行下来没啥问题 ...
异步肯定要判断是否成功啊……
成功成功处理,失败失败的处理……
newstart
发表于 2023-6-6 13:50:03
jarlyyn 发表于 2023-6-6 01:42 PM
异步肯定要判断是否成功啊……
成功成功处理,失败失败的处理……
那是当然,异步情况下必须要处理
所以我用自己开发的客户端,实现了一个Command类,专门处理这种情况
Command相当于Alias+Trigger+Timer的组合
异步执行后,有4种不同状态结果,分别为成功、失败、重试、超时,所有的处理都要在Command类型里面完成的
app在https://github.com/crapex/pymud
异步Command可以看object.py中的SimpleCommand实现
所以,针对移动这个命令,我的实现对上述所有情况都进行了处理
self.cmd_move = Configuration.CmdMove(self.session, self.mapper, id = "cmd_move", succ_tri = self.tri_loc, fail_tri = self.triggersInGroup("movefail"), retry_tri = self.triggersInGroup("moveretry"))
jarlyyn
发表于 2023-6-6 14:23:47
newstart 发表于 2023-6-6 01:50 PM
那是当然,异步情况下必须要处理
所以我用自己开发的客户端,实现了一个Command类,专门处理这种情况
Com ...
这个应该是两个概念吧。
异步是异步,封装是封装。
异步一般目前比较常见的是类似js里Promoise的概念
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
myPromise
.then((value) => `${value} and bar`)
.then((value) => `${value} and bar again`)
.then((value) => `${value} and again`)
.then((value) => `${value} and again`)
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
});
封装的话,我这里主要是把tri,alias,timer处理成envent,然后就是标准的有限状态擅长的领域了。
提这个主要是觉得1和2的作用域未必是完全匹配的。
newstart
发表于 2023-6-6 15:20:17
本帖最后由 newstart 于 2023-6-6 03:28 PM 编辑
jarlyyn 发表于 2023-6-6 02:23 PM
这个应该是两个概念吧。
异步是异步,封装是封装。
可能是我表述的问题。
python的asyncio就是异步IO的标准库,和你说的promise是类似的,Python里面是使用asyncio.Future类来实现类似功能的
我前面说的是,Command相当于Alias+Trigger+Timer的组合,这里是相当于,确实可以理解成几个类型的封装,
但最大的核心区别,我的Trigger实现是既支持同步(回调函数模式),也支持异步(协程/事件循环模式)。
我在实现的Command类里面,所有对触发器的处理,都是使用异步事件模式实现的,其核心Command.execute方法也是使用async关键词修饰的,也是属于异步命令
同步情况下:
<font size="2"><font color="#000000">if tri.matched(msg):</font></font>
<font size="2"><font color="#000000"> do_something</font></font>
异步情况下:
<font size="2"><font color="#000000">class Trigger:</font></font>
<font size="2"><font color="#000000"> # trigger的异步等待方式</font></font>
<font size="2"><font color="#000000"> async def triggered(self):</font></font>
<font size="2"><font color="#000000"> self.event.clear()</font></font>
<font size="2"><font color="#000000"> await self.event.wait()</font></font>
<font size="2"><font color="#000000"> return trigger_capture_info</font></font>
在异步调用Trigger时,是使用下面的函数方式:
<font size="2"><font color="#000000">async def execute():</font></font>
<font size="2"><font color="#000000"> write_some_command</font></font>
<font size="2"><font color="#000000"> await tri.triggered()</font></font>
<font size="2"><font color="#000000"> do_something</font></font>执行上述execute函数只能在async定义的异步函数体中执行,或者使用asyncio.create_task创建任务,放到事件循环中执行。
python的asyncio库介绍:
https://docs.python.org/zh-cn/3.10/library/asyncio.html
Python异步协程(asyncio详解)
https://www.cnblogs.com/Red-Sun/p/16934843.html
jarlyyn
发表于 2023-6-6 16:05:07
newstart 发表于 2023-6-6 03:20 PM
可能是我表述的问题。
python的asyncio就是异步IO的标准库,和你说的promise是类似的,Python里面是使用as ...
promise是promise,async是async。
js的异步发展史就是 callback->promise->async这条线的三个阶段。
promise的主要特点是可以串起来,可以有 解决/异常两条分路,以及可以储存和管理。
建立了promise并不一定执行
然后是async的问题
包括你引用的文档也写的很清楚
aysnc(和线程)解决的是并发问题。
并发问题是什么意思呢?
以房间名的触发为例。
[*]看见房间名,触发一个async,起了一个协程。
[*]又看到一次房间名,又触发一个sync,又起了一个携程。
[*]这时候你拥有了两个优先级相同的,并发的协程,都在等待npc信息。
[*]看到npc了。两个协程都触发了,会发两次kill,然后两个协程都结束。
我不知道你的客户端架构是怎么样的。但一般mush的lua机器人我是不建议用异步库的,容易出问题。
一般来说,我更建议和浏览器环境下的js一样,实际上的单线程,并发/协程由客户端主程序去维护,并防止数据竞争。
newstart
发表于 2023-6-6 19:24:32
本帖最后由 newstart 于 2023-6-6 07:30 PM 编辑
jarlyyn 发表于 2023-6-6 04:05 PM
promise是promise,async是async。
js的异步发展史就是 callback->promise->async这条线的三个阶段。
emmmm....以下继续进行技术探讨:)
我的理解和你的理解可能有些差异,并发是与并行来对应的;异步是与同步来对应的,这两者之间没有矛盾吧...
MushClient是不能很好的支持python异步的,如果lua可以用协程实现,但使用python时,只能使用基于生成器的协程(我以前就是这么干的),如果想使用async/await,由于两者之间不在同一个事件循环中(也就是不同的线程中),所以根本不能支持async/await语法运行
也是基于这个原因,我决定开发自己的客户端,以完整利用async/await的特性。
再解释下,我的客户端架构就是单线程的,异步单线程,事件循环由主对象PyMud进行维护;
客户端实现的Trigger的异步处理函数并非在Trigger被触发时调用,Trigger触发时仅会调用同步函数,在同步函数中,会设置其可等待的Future对象(我使用的asyncio.Event)的状态为done;
异步的async def triggered()仅在被函数代码await tri.triggered()调用时,才产生协程并予以运行...而这个等待的代码只有在Command的异步执行函数中才会被调用。
以你说的房间名举例:
tri.triggered()是一个协程,并不会在收到房间名时被调用,它仅会在Command的look指令发出后,被await tri.triggered()代码执行时才会被调用,以等待房间名,其状态直接会通过语句结果返回,举例:
class CmdLook(Command):
# 其他代码省略,仅留下execute执行函数
async def execute(self, cmd = "look", *args, **kwargs):
try:
self.reset()
# 1. 输入命令
self.tri_start.enabled = True
self.tri_exits.reset()
self.session.writeline(cmd)
# 2. 等待房间出口被触发
done, pending = await asyncio.wait(, timeout = 5)
if len(done) > 0:
task = tuple(done)
state = task.result()
# 此处原有room 的有关数据设定,此处删除了
# 执行默认onSuccess
self._onSuccess(room)
result = self.SUCCESS
else:
# timeout,同success
self._onTimeout()
result = self.TIMEOUT
self.reset()
return self.State(result, room)
except Exception as e:
self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}")
self.error(f"异常追踪为: {traceback.format_exc()}")
这个不是与promise.then().catch()一样吗?
另外,你举例房间名触发的情况,当然,如果连续两次look,的确会触发两次协程,如果输入命令的速度过快,两次协程都会在第一次房间名出来的时候执行完毕,第二次则不会响应;
对于这个的处理方式,每一个Command实现时,其内部的awaitable对象都被存储,在下一次命令被执行时,如果上一个awaitable对象(如tri.triggered()协程)还处于pending状态,则会手动取消,以确保每一个Command只有一个awaitable对象起作用。
另外,我又在网上搜索了有关并发、并行、同步、异步的区别;以及promise、async/await的区别。js的新版中也是async/await,就如你说的js的异步发展史就是 callback->promise->async这条线的三个阶段。
网上说所,js中,async/await更像是一种语法糖,async定义的函数就是一个promise对象,是promise对象的改良版
参考资料:
1.1、程序中所谓「异步」和「并发」的区别有哪些:https://worktile.com/kb/ask/38122.html
1.2、区别:并行、并发、同步、异步: https://blog.csdn.net/wangxiaosu0501/article/details/124602000
2.1、promise和async await区别(我觉得这一篇写的特别完整):https://www.jianshu.com/p/0431e209dc0f
2.2、Promise和async/await的区别:https://blog.csdn.net/weixin_47277125/article/details/123754080
2.3、promise和async await的区别:https://blog.csdn.net/weixin_44246717/article/details/118659810
2.4、promise和async await的区别:https://blog.csdn.net/sinat_36728518/article/details/107293428?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-107293428-blog-118659810.235^v38^pc_relevant_anti_vip_base&spm=1001.2101.3001.4242.1&utm_relevant_index=1
jarlyyn
发表于 2023-6-6 19:48:37
newstart 发表于 2023-6-6 07:24 PM
emmmm....以下继续进行技术探讨:)
我的理解和你的理解可能有些差异,并发是与并行来对应的;异步是与同步 ...
在我的理解里
promise 只是一个数据结构(callback的队列)。并不需要执行。
promise可以用async去消费他,理论也可以用自己 方式自行消费。
提到async/协程的问题就是,协程在我理解里就是一个在同一个线程里分配cpu事件的机制,挂起当前协程。
而mud机器人如果使用挂起的机制的话,复杂度很高(牵涉到怎么保证取消挂起),所以我才会觉得有问题。
newstart
发表于 2023-6-6 21:36:52
本帖最后由 newstart 于 2023-6-6 09:41 PM 编辑
jarlyyn 发表于 2023-6-6 07:48 PM
在我的理解里
promise 只是一个数据结构(callback的队列)。并不需要执行。
我查了下,在js中,promise对象也是使用await语句来进行消费,与async定义的对象一样,而且js与python的代码逻辑几乎完全一致
所以上面是一样的啊,async定义的函数也只是一个数据结构,不需要执行,需要使用await去消费;
如async函数定义
async def func():
print('aaa')
async def main():
#下面这句不会调用函数输出aaa,只会生成一个包含func()函数的协程对象
obj = func()
#下面这句才会真正执行func函数,输出aaa
await obj
# 主入口
asyncio.run(main())
协程对象obj只会执行一次,执行完毕后其状态会自动从pending变成done,然后无法再次运行
我理解你上面说的还是传统的协程实现,在lua里,这种实现方式是
使用coroutine.create()来创建,
使用coroutine.yield()来等待
使用coroutine.resume()来恢复
下面是我以前写的lua携程代码(MushClient)
function Room:CatchStart(cmd, after)
if cmd == nil or cmd == "" then cmd = "look" end
if after == nil or after <= 0.1 then after = 0.1 end
self:Update()
Room.catch_co = coroutine.create(function()
world.EnableTriggerGroup("gps.catchroom",false)
world.EnableTrigger("Room_delay",true)
world.EnableTrigger("Room_name",true)
world.DoAfter(after, cmd)
world.EnableTimer("Room_overtimer", true)
world.ResetTimer("Room_overtimer")
local r = coroutine.yield() --挂起
if r then
self:CatchEnd()
else
self:CatchFail()
end
end)
coroutine.resume(Room.catch_co)
end
在python以前的版本里,使用基于生成器的协程,是
带有yield指令的函数作为协程
使用next调用协程推进(恢复)
使用异常捕获StopIteration来确认其中止状态
下面是我以前写的python协程代码(MushClient)
<div>
</div><div> def _onSuccess(self, sender, args):
try:
self._generator.send((CommandState.Success, sender, args))
except StopIteration:
#print("the command: '%s' has executed successfully" % self._command)
pass
def _onFail(self, sender, args):
try:
self._generator.send((CommandState.Failed, sender, args))
except StopIteration:
#print("the command: '%s' hasn't executed failed" % self._command)
pass
def _onTimeout(self, sender, args):
try:
self._generator.send((CommandState.Timeout, sender, args))
except StopIteration:
#print("the command: '%s' hasn't executed timeout" .% self._command)
pass
def _beforeExecute(self, **params):
'''
default before execute:
enable the group
'''
en = params.get('autoenable', True)
self.Enable(en)
def _afterExecute(self, state):
self.Enable(False)
self._state = state
self._doEvent("AfterDone")
self._state = CommandState.NotStart
def Enable(self, value = True):
self._enabled = value
for tri in self._triggers.keys():
self._triggers.Enabled = value
def _coroutine(self):
state, _, _ = yield
self._afterExecute(state)
def Execute(self, cmd, **params):
self._command = cmd # command text
self._beforeExecute(**params)
self._generator = self._coroutine()
next(self._generator)
self.mush.Execute(self._command)</div>
在上面这几种情况下,挂起/恢复/取消机制非常复杂,写出来的代码基本没有可读性
后来版本升级后,可以@asyncio.coroutine标记协程和yield from语法调用(3.3版为async/yield from),但还是很难编写和读懂代码逻辑
因此,Python自3.5版引入了async/await语法(js是es7中引入的),就是希望将基于协程的异步实现的可以像同步函数一样
其实现逻辑是将未来产生的对象使用Future类(一翻译为期物,指被期待的物体)进行封装,这样就可以由事件循环自动维护协程的运行/等待/中止状态
下面是我现在写的异步代码(PyMUD,自动少林跳楼)
async def jumptower(self):
"跳楼轻功主循环"
result = await self._runto.execute('rt {}'.format(self.PLACE_TOWER))
if result == self.SUCCESS:
self._triggers["skm_levelup"].enabled = True
self._triggers["skm_towersq"].enabled = True
self._triggers["skm_towerup"].enabled = True
self.session.writeline("set brief 3")
dir = "enter"
times = 0
while True:
awt_tasks = []
awt_tasks.append(asyncio.create_task(self._triggers["skm_towersq"].triggered(), name = "tsk_towersq"))
awt_tasks.append(asyncio.create_task(self._triggers["skm_towerup"].triggered(), name = "tsk_towerup"))
#await self._move.execute(dir)
self.session.writeline(dir)
done, pending = await asyncio.wait(awt_tasks, return_when = "FIRST_COMPLETED")
for task in list(pending):
task.cancel()
tasks_done = list(done)
iflen(tasks_done) == 1:
task = tasks_done
state, name, line, wildcards = task.result()
if name == self._triggers["skm_towerup"].id:
await asyncio.sleep(0.1)
floor = wildcards
if floor == "七":
self.session.writeline("exert regenerate")
self.session.writeline("exert recover")
dir = "out"
times += 1
if times >= 100:
times = 0
await self._lifemisc.execute("feed")
else:
dir = "up"
elif name == self._triggers["skm_towersq"].id:
# 判断技能等级是否已达上限
if self._levelup:
self._levelup = False
await self._skills.execute("skills") # 等待以使skills命令获取完相关技能等级
dodge = self.session.getVariable("dodge", None)
if dodge:
dodge_lvl = dodge
dodge_max = dodge
if dodge_lvl >= dodge_max:
self.info(f"基本轻功已达等级上限 {dodge_max}", '技能')
break
await asyncio.sleep(2)
dir = "enter"
if self._halted:
self.info("跳楼被手动中止", '技能')
break
self._triggers["skm_levelup"].enabled = False
self._triggers["skm_towersq"].enabled = False
self._triggers["skm_towerup"].enabled = False
return self.SUCCESS
else:
self.error(f"未抵达跳楼起始点 {self.PLACE_TOWER}, 请检查重试", '技能')
return self.FAILURE
不同的协程实现方式,都可以实现同样的功能,但是代码编写难度和可读性完全是天差地别
在下面的js异步发展史中提到,promise本质上并没有解决js的回调地狱问题,里面这句话说的很好:
async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步编程看起来像同步一样。
异步编程的最高境界就是不用关心他是不是异步,因此async/await被很多人为是异步编程的终极解决方案。
异步编程 101:Python async await发展简史:https://blog.csdn.net/weixin_34397291/article/details/91401107
js异步发展史:https://blog.csdn.net/qq_36850967/article/details/93745949
jarlyyn
发表于 2023-6-6 21:44:28
newstart 发表于 2023-6-6 09:36 PM
我查了下,在js中,promise对象也是使用await语句来进行消费,与async定义的对象一样,而且js与python的代 ...
那我的疑惑是,这里真的需要协程么?
举个例子,我的跳塔代码
(function (App) {
const Round = 10
App.Quest.Tiaota = {}
App.Quest.Tiaota.Data = {}
App.Quest.Tiaota.Start = function (param) {
App.Quest.Tiaota.Data = {
Count: 0,
}
if (param) {
param = param.trim()
}
let max = 0
if (param) {
let pmax = param - 0
if (isNaN(pmax)) {
throw "跳塔参数" + param + "必须为最大dodge等级"
}
max = pmax
}
let skill = App.Core.PlayerGetSkillByID("dodge")
if (skill != null) {
if (max <= 0) {
max = skill.Max
}
if (skill.Level >= max) {
App.Fail()
return
}
}
App.Commands([
App.NewCommand('prepare', App.PrapareFull),
App.NewCommand("to", App.Options.NewWalk("sls-zlxy")),
App.NewCommand("nobusy"),
App.NewCommand("function", App.Quest.Tiaota.Tiao),
App.NewCommand('do', "skills"),
App.NewCommand("nobusy"),
]).Push()
App.Next()
}
App.Quest.Tiaota.Tiao = function () {
App.Quest.Tiaota.Data.Count++
if (App.Quest.Tiaota.Data.Count > Round) {
App.Next()
return
}
if (!App.Stopped) {
App.Commands([
App.NewCommand("move", App.Options.NewPath("enter;u;u;u;u;u;u;out")),
App.NewCommand("nobusy"),
App.NewCommand("function", App.Quest.Tiaota.Tiao),
]).Push()
}
App.Next()
}
})(App)App.Commands就是一个类似于Promise的结构。
App.Next()就是执行队列。
有个App.Fail()就是进行失败处理。
并不需要async机制。在mush里也可以用lua/js/python跑出来。
zhuzi
发表于 2023-6-6 21:49:15
高手论战
已star,持续关注