Python:同时等待异步协程和同步功能

时间:2020-06-14 13:59:22

标签: python-3.x async-await python-asyncio python-multithreading

我想在执行同步功能期间建立SSH SOCKs隧道(使用asyncssh)。功能完成后,我想拆除隧道并退出。

很显然,必须等待一些异步功能以使隧道正常工作,因此重要的是conn.wait_closed()和同步功能是同时执行的。所以我很确定我确实需要第二个线程。 我首先使用ThreadPoolExecutorrun_in_executor尝试了一些更聪明的方法,但最终遇到了下面的糟糕透顶的变体。

#! /usr/bin/env python3

import traceback
from threading import Thread
from concurrent.futures import ThreadPoolExecutor

import asyncio, asyncssh, sys

_server="127.0.0.1"
_port=22
_proxy_port=8080


async def run_client():
    conn = await asyncio.wait_for(
        asyncssh.connect(
            _server,
            port=_port,
            options=asyncssh.SSHClientConnectionOptions(client_host_keysign=True),
        ),
        10,
    )

    listener = await conn.forward_socks('127.0.0.1', _proxy_port)
    return conn

async def do_stuff(func):
    try:
        conn = await run_client()
        print("SSH tunnel active")

        def start_loop(loop):
            asyncio.set_event_loop(loop)
            try:
                loop.run_forever()
            except Exception as e:
                print(f"worker loop: {e}")

        async def thread_func():
            ret=await func()
            print("Func done - tearing done worker thread and SSH connection")
            conn.close()
            #  asyncio.get_event_loop().stop()
            return ret

        func_loop = asyncio.new_event_loop()
        func_thread = Thread(target=start_loop, args=(func_loop,))
        func_thread.start()
        print("thread started")
        fut = asyncio.run_coroutine_threadsafe(thread_func(), func_loop)
        print(f"fut scheduled: {fut}")

        done = await asyncio.gather(asyncio.wrap_future(fut), conn.wait_closed())
        print("wait done")
        for ret in done:
            print(f"ret={ret}")

        # Canceling pending tasks and stopping the loop
        #  asyncio.gather(*asyncio.Task.all_tasks()).cancel()

        print("stopping func_loop")
        func_loop.call_soon_threadsafe(func_loop.stop())
        print("joining func_thread")
        func_thread.join()
        print("joined func_thread")

    except (OSError, asyncssh.Error) as exc:
        sys.exit('SSH connection failed: ' + str(exc))
    except (Exception) as exc:
        sys.exit('Unhandled exception: ' + str(exc))
        traceback.print_exc()


async def just_wait():
    print("starting just_wait")
    input()
    print("ending just_wait")
    return 42

asyncio.get_event_loop().run_until_complete(do_stuff(just_wait))

实际上,它“正确”地“正常”运行,直到join设置工作线程时出现异常为止。我猜想是因为我做的事情不是线程安全的。

Exception in callback None()
handle: <Handle>
Traceback (most recent call last):
  File "/usr/lib/python3.7/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
TypeError: 'NoneType' object is not callable

要测试代码,您必须具有运行本地SSH服务器并为您的用户设置密钥文件。您可能需要更改_port变量。

我正在寻找异常的原因和/或程序版本,该版本在线程中需要较少的人工干预,并且可能仅使用单个事件循环。当我想await两件事时(例如在asyncio.gather通话中),我不知道如何实现后者。

1 个答案:

答案 0 :(得分:2)

直接导致错误的原因是此行:

func_loop.call_soon_threadsafe(func_loop.stop())

目的是在运行func_loop.stop()事件循环的线程中调用func_loop。但是按照编写,它会在当前线程中调用func_loop.stop() ,并将其返回值(None)传递给call_soon_threadsafe作为要调用的函数。这导致call_soon_threadsafe抱怨无人可呼。要解决当前的问题,您应该删除多余的括号并以func_loop.call_soon_threadsafe(func_loop.stop)的形式调用该方法。

但是,代码在编写时绝对过于复杂:

  • 当您已经在事件循环中时,创建新的事件循环是没有意义的
  • just_wait不应为async def,因为它不会等待任何东西,因此显然不是异步的。
  • sys.exit采用整数退出状态,而不是字符串。另外,尝试在对sys.exit的调用后 打印回溯记录没有多大意义。

要从asyncio运行非异步功能,只需将run_in_executor与该功能一起使用,并将其原样传递给非异步功能。您不需要额外的线程,也不需要额外的事件循环,run_in_executor将负责处理该线程并将其与当前事件循环连接,从而有效地使sync函数处于等待状态。例如(未试用):

async def do_stuff(func):
    conn = await run_client()
    print("SSH tunnel active")
    loop = asyncio.get_event_loop()
    ret = await loop.run_in_executor(None, func)
    print(f"ret={ret}")
    conn.close()
    await conn.wait_closed()
    print("wait done")

def just_wait():
    # just_wait is a regular function; it can call blocking code,
    # but it cannot await
    print("starting just_wait")
    input()
    print("ending just_wait")
    return 42

asyncio.get_event_loop().run_until_complete(do_stuff(just_wait))

如果您需要等待just_wait中的内容,可以将其设置为async,并使用run_in_executor作为其中的实际阻止代码:

async def do_stuff():
    conn = await run_client()
    print("SSH tunnel active")
    loop = asyncio.get_event_loop()
    ret = await just_wait()
    print(f"ret={ret}")
    conn.close()
    await conn.wait_closed()
    print("wait done")

async def just_wait():
    # just_wait is an async function, it can await, but
    # must invoke blocking code through run_in_executor
    print("starting just_wait")
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, input)
    print("ending just_wait")
    return 42

asyncio.run(do_stuff())