【问题标题】:Python asyncio - Loop exits with Task was destroyed but it is pendingPython asyncio - 循环退出任务被破坏但它正在等待
【发布时间】:2016-06-25 00:28:16
【问题描述】:

这是我的python程序的相关代码:

import discord
import asyncio

class Bot(discord.Client):
    def __init__(self):
        super().__init__()

    @asyncio.coroutine
    def my_background_task(self):
        yield from self.wait_until_ready()
        while not self.is_closed:
            yield from asyncio.sleep(3600*24) # <- This is line 76 where it fails
            doSomething()

bot = Bot()
loop = asyncio.get_event_loop()
try:
    loop.create_task(bot.my_background_task())
    loop.run_until_complete(bot.login('username', 'password'))
    loop.run_until_complete(bot.connect())
except Exception:
    loop.run_until_complete(bot.close())
finally:
    loop.close()

程序偶尔会退出(自行退出,但它不应该退出)除了

之外没有其他错误或警告
Task was destroyed but it is pending!
task: <Task pending coro=<my_background_task() running at bin/discordBot.py:76> wait_for=<Future pending cb=[Task._wakeup()]>>

如何保证程序不会随意退出?我在 Xubuntu 15.10 上有 Python 3.4.3+。

【问题讨论】:

    标签: python-3.x python-asyncio discord discord.py


    【解决方案1】:

    这是因为 Discord 客户端模块每分钟左右需要控制一次。

    这意味着任何窃取控制权超过一定时间的函数都会导致 Discord 的客户端进入无效状态(这将在稍后的某个时间表现为异常,可能在客户端的下一个方法调用时)。

    为了保证discord模块客户端能ping通discord服务器,应该使用真正的多线程解决方案。

    一种解决方案是将所有繁重的处理转移到一个单独的进程(单独的线程不会这样做,因为 Python 具有全局解释器锁),并将不和谐机器人用作一个薄层,其职责是填充工作队列。

    相关阅读: https://discordpy.readthedocs.io/en/latest/faq.html#what-does-blocking-mean

    示例解决方案...这超出了问题的范围,但我已经编写了大部分代码。如果我有更多时间,我会写一个更短的解决方案:)

    2部分,discord交互和处理服务器:

    这是不和谐的监听器。

    import discord
    import re
    import asyncio
    import traceback
    
    import websockets
    import json
    
    # Call a function on other server
    async def call(methodName, *args, **kwargs):
        async with websockets.connect('ws://localhost:9001/meow') as websocket:
            payload = json.dumps( {"method":methodName, "args":args, "kwargs": kwargs})
            await websocket.send(payload)
            #...
            resp = await websocket.recv()
            #...
            return resp
    
    client = discord.Client()
    tok = open("token.dat").read()
    
    @client.event
    async def on_ready():
        print('Logged in as')
        print(client.user.name)
        print(client.user.id)
        print('------')
    
    @client.event
    async def on_error(event, *args, **kwargs):
        print("Error?")
    
    @client.event
    async def on_message(message):
        try:
            if message.author.id == client.user.id:
                return
            m = re.match("(\w+) for (\d+).*?", message.content)
            if m:
                g = m.groups(1)
                methodName = g[0]
                someNumber = int(g[1])
                response = await call(methodName, someNumber)
                if response:
                    await client.send_message(message.channel, response[0:2000])
        except Exception as e:
            print (e)
            print (traceback.format_exc())
    
    client.run(tok)
    

    这是处理繁重请求的工作服务器。您可以使这部分同步或异步。

    我选择使用一种称为 websocket 的魔法将数据从一个 python 进程发送到另一个。但是你可以使用任何你想要的东西。例如,您可以让一个脚本将文件写入目录,而另一个脚本可以读取文件并处理它们。

    import tornado
    import tornado.websocket
    import tornado.httpserver
    import json
    import asyncio
    import inspect
    import time
    
    class Handler:
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        def consume(self, text):
            return "You said {0} and I say hiya".format(text)
    
        async def sweeps(self, len):
            await asyncio.sleep(len)
            return "Slept for {0} seconds asynchronously!".format(len)
    
        def sleeps(self, len):
            time.sleep(len)
            return "Slept for {0} seconds synchronously!".format(len)
    
    
    class MyService(Handler, tornado.websocket.WebSocketHandler):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        def stop(self):
            Handler.server.stop()
    
        def open(self):
            print("WebSocket opened")
    
        def on_message(self, message):
            print (message)
            j = json.loads(message)
            methodName = j["method"]
            args = j.get("args", ())
    
            method = getattr(self, methodName)
            if inspect.iscoroutinefunction(method):
                loop = asyncio.get_event_loop()
                task = loop.create_task(method(*args))
                task.add_done_callback( lambda res: self.write_message(res.result()))
                future = asyncio.ensure_future(task)
    
            elif method:
                resp = method(*args)
                self.write_message(resp)
    
        def on_close(self):
            print("WebSocket closed")
    
    application = tornado.web.Application([
        (r'/meow', MyService),
    ])
    
    if __name__ == "__main__":
        from tornado.platform.asyncio import AsyncIOMainLoop
        AsyncIOMainLoop().install()
    
        http_server = tornado.httpserver.HTTPServer(application)
        Handler.server = http_server
        http_server.listen(9001)
    
        asyncio.get_event_loop().run_forever()
    

    现在,如果您在单独的 python 脚本中运行这两个进程,并告诉您的机器人“睡眠 100 秒”,它会愉快地睡眠 100 秒! asyncio 的东西用作临时工作队列,您可以通过将侦听器作为单独的 python 脚本运行来正确地将侦听器与后端处理分开。

    现在,无论您的函数在“服务器”部分运行多长时间,都将永远不会阻止客户端部分 ping 不和谐服务器。

    图片上传失败,但是......无论如何,这是告诉机器人睡眠和回复的方法......注意睡眠是同步的。 http://i.imgur.com/N4ZPPbB.png

    【讨论】:

    • 感谢您对 discord 工作的洞察力。不幸的是,我不是线程和异步编程方面的专家。您能否再解释一下这个“薄层”是什么以及如何实现它?
    • 有很多方法,这个讨论超出了这个问题的范围。我会浏览一下我自己的个人代码(这很糟糕......因为我写了 ot),看看我是否可以为你提取一些 sn-ps 和想法:)
    • 感谢您通过示例进行详尽的解释。
    【解决方案2】:

    我认为asyncio.sleep 不会出现问题。无论如何,你不应该压制你得到的异常:

    bot = Bot()
    loop = asyncio.get_event_loop()
    try:
        # ...
    except Exception as e:
        loop.run_until_complete(bot.close())
        raise e  # <--- reraise exception you got while execution to see it (or log it here)
    finally:
        # ...
    

    【讨论】:

    • 虽然这确实回答了问题,但我怀疑问题实际上并没有在问题中正确提出。我不想把话放到提问者的嘴里走得太远,但我认为真正被问到的是“如何确保程序不会随机退出?”不将问题的范围限制在睡眠本身。
    • @JamEnergy 你是对的,我已经编辑了这个问题。
    【解决方案3】:

    您必须在退出时手动停止任务:

    import discord
    import asyncio
    
    class Bot(discord.Client):
        def __init__(self):
            super().__init__()
    
        @asyncio.coroutine
        def my_background_task(self):
            yield from self.wait_until_ready()
            while not self.is_closed:
                yield from asyncio.sleep(3600*24) # <- This is line 76 where it fails
                doSomething()
    
    bot = Bot()
    loop = asyncio.get_event_loop()
    try:
        task = loop.create_task(bot.my_background_task())
        loop.run_until_complete(bot.login('username', 'password'))
        loop.run_until_complete(bot.connect())
    except Exception:
        loop.run_until_complete(bot.close())
    finally:
        task.cancel()
        try:
            loop.run_until_complete(task)
        except Exception:
            pass 
        loop.close()
    

    【讨论】:

    • 我的程序不应该退出,它应该无限期地运行并且每天运行一次 doSomething() 函数(除其他外)。
    • 但是您的程序肯定没有优雅地完成后台任务,这会在事件循环关闭时产生警告文本。我建议适当取消后台任务以防止它发生。
    • 是的,我将包含您的 cancel() 程序,但是它不能解决我的问题 - 程序意外退出。有人建议我心跳间隔可能是问题所在。是否有可能以及如何解决?
    • 对不起,我有一个误解——我以为你知道你的程序为什么退出。尝试打印第一个异常import traceback; traceback.print_exc()——它可能会给你一个线索。
    猜你喜欢
    • 2017-12-17
    • 2016-02-29
    • 2019-08-09
    • 2017-01-08
    • 1970-01-01
    • 2021-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多