异步进程


这是进阶内容。


同步与异步

大多数代码的执行是同步的,也就是说代码中的所有语句都是在前一条执行完毕后才执行后一条的。这让代码易于理解。在很多情况下这也是必须的要求,因为后面的代码需要依靠前面的计算结果或定义才能执行。

看看这段代码:
    print "执行前..."
    long_running_function() # 执行时间很长的函数
    print "执行后..."
运行时,会先打印“执行前...”,然后 long_running_function 开始工作,不管需要运行多长时间,只有在它执行完毕之后系统才会打印“执行后...”。这很简单,而且逻辑清晰。Evennia的大部分代码是这样工作的。大多数时候,我们希望确保命令严格按顺序执行。

但问题是 Evennia 是一个多用户服务器。玩家输入的命令是依次到达的,服务器会在不同玩家的命令间迅速切换执行。所以如果某个用户的命令中包含了那个执行时间很长的函数,其他所有玩家都要被迫等待,直到它执行完毕......这不是一个好的解决方案。

对如今的计算机系统来说,这可能不算什么问题,因为只有极少数命令的执行时间会长到影响其他用户的感受。如前面提到的,大多数时候你还是希望让命令严格按顺序执行。

如果执行命令的延迟真的变得很明显,而且命令实际执行完毕的次序无关紧要,那你可以让它异步执行。这要用到 src/utils/utils.py 中的 run_async() 函数。
    from ev import utils
    print "执行前..."
    utils.run_async(long_running_function)
    print "执行后..."
现在你会发现程序不再等待 long_running_function 执行完毕,可以看到直接打印出“执行前...”和“执行后...”。执行时间很长的函数会在后台运行,你(和其他用户)可以继续做别的事情。


自定义异步操作

使用异步调用的一个难点是如何处理返回值。如果你需要使用 long_running_function 的返回值该怎么办?把代码放在 long_running_function 之后来处理它的返回值是没有实际意义的,就像我们所看到的,“执行后...”在 long_running_function 完成之前就打印出来了,使得这行语句在处理函数返回值方面毫无用处。因此必须使用回调。

utils.run_async 有一些保留参数。
  • at_return(r)(回调)会在异步函数(上面的 long_running_function)成功执行完成时调用。参数 r 是函数的返回值(或 None)。例如:
  •         def at_return(r):
                print r
    
  • at_return_kwargs —— 可选的字典,作为 at_return 回调函数的关键字参数。
  • at_err(e) (异常回调)如果异步函数执行失败并引发异常。这个异常会封装在失败对象 e 中传给 errback。如果你没有提供自己的异常回调函数,Evennia会自动添加一个,它会将错误信息写入evennia的日志中。异常回调函数的例子如下:
  •         def at_err(e): 
                print "There was an error:", str(e)
    
  • at_err_kwargs —— 可选的字典,作为 at_err 异常回调函数的关键字参数。

命令定义内部使用异步调用的例子:
    from ev import utils
    from game.gamesrc.commands.basecommand import Command

    class CmdAsync(Command):

        key = "asynccommand"

        def func(self):

            def long_running_function(): 
                #[... 许多费时的代码
                return final_value

            def at_return(r):
                self.caller.msg("The final value is %s" % r)

            def at_err(e):
                self.caller.msg("There was an error: %s" % e)

            # 执行异步调用,设置回调函数
            utils.run_async(long_running_function, at_return, at_err)
就是这样,从现在起我们可以忘掉 long_running_function 继续做其它需要做的事情了。当这个函数执行完毕时会调用 at_return 函数,并把最终的值弹出来给我们看。如果发生错误,我们会看到一条错误信息。


进程池

ProcPool 是Evennia的子系统,它会在 ampoule 包(已包含在evennia中)的基础上创建一个进程池。当它活动时,run_async 会使用它来执行命令。在默认情况下 ProcPool 是不活动的,你可以通过 settings.PROCPOOL_ENABLED 开启它。但需要注意,默认的 SQLite3 数据库不适用于多进程环境。所以如果你想用 ProcPool,你应该考虑改用别的数据库,如 MySQL 或 PostgreSQL。

进程池给 run_async 提供了一些额外的选项。以下的关键字参数在 ProcPool 活动时有效:
  • use_thread —— 它强制恢复到线程操作(见上文)。它可以有效地停止 ProcPool 的所有附加功能。
  • proc_timeout —— 它可以强制指定进程的超时时间(秒数),超过时间的进程会被终止。
  • at_return,at_err —— 它们的功能与前文所述相同。

第一个参数除了可以是供 run_async 调用的函数名外,还可以是源代码字符串。通过 ProcPool 这段Python源代码可以在子进程中执行。提供给 run_async 的保留参数之外的额外关键字参数会被用来指定哪些东西可在执行环境中使用。

在异步执行中有一个特殊的变量:_return。这是一个函数,所有提供给 _return 的数据都会从执行环境中返回,并作为 at_return 回调的参数(如果定义过)。你可以在代码中多次调用 _return,返回值会合成一个列表。

例子:
    from src.utils.utils import run_async

    source = """
    from time import sleep
    sleep(5) # 暂停五秒钟
    val = testvar + 5
    _return(val)
    _return(val + 5)
    """

    # 我们假定 myobj 是之前取出的角色
    # 这些回调函数只会显示结果或错误信息
    def callback(ret):
        myobj.msg(ret)
    def errback(err):
        myobj.msg(err)
    testvar = 3

    # 异步执行
    run_async(source, at_return=callback, at_err=errback, testvar=testvar)

    # 这会返回 '[8, 13]'

你还可以在游戏中使用 @py 命令测试异步机制:
 @py from src.utils.utils import run_async;run_async("_return(1+2)",at_return=self.msg)

注意:代码的执行不会受任何安全检查,所以不应将它提供给非特权用户使用。不受信任的用户可以使用 contrib.evlang.evlang.limited_exec,它运行的是受到严格限制的 Python 版本。它在幕后会使用 run_async。


延时

delay 函数和 run_async 相仿,但要简单得多。它的实际功能只是延迟指令的执行直到指定的时间。这类似于 time.sleep(),但 delay 是异步的,而 sleep 会在整个持续期间锁住整个服务器。
    def callback(obj):
        obj.msg("Returning!")
    delay(10, caller, callback=callback)
这会延迟10秒执行 callback。在持续时间命令的教程中有关于这个函数的更多探讨。


杂项笔记

请注意,run_async 会在幕后尝试建立一个单独的线程。有些数据库,特别是我们默认的 SQLite3 数据库不允许并行的读写操作。所以如果在你的函数中有较多的数据库访问(比如保存属性),如果你随意使用这个函数可能会让服务器的实际运行速度变慢。建议你在这方面做广泛的测试。

总之,要谨慎地选择使用异步调用的时机。它主要适用于那些对游戏世界没有直接影响的大型管理操作(比如导入和备份操作)。由于无法准确预测异步调用的实际结束时间,将它用在游戏命令中会增加潜在的混乱和不一致性(而且bug很难重现)。

本文的第一个同步例子在 Twisted 中的运行结果不太准确,因为 Twisted 本身就是一个异步服务器。你可能会发现第一个“执行前...”不会被马上打印出来。相反,所有的文本可能都要延迟到长时间执行的函数完毕之后才会打印出来。所有命令都会按预期的相对顺序执行,但它们可能会延迟或成组出现。


扩展阅读

从技术上讲,run_async 只是对 Twisted Deferred 对象做了很简单的封装,在封装中建立了一个单独的线程,并且如果用户没有提供 errback,会指定一个默认的 errback。如果你明白你所做的事情,你可以绕过这个工具函数,按自己的喜好建立更复杂的回调链。


(原文:https://github.com/Evennia/evennia/wiki/Async%20Process    翻译:卢铱俊)