分享一下完全不依赖 asyncio 也支持异步语法的库

2021-09-07 10:15:43 +08:00
 sujin190

https://github.com/snower/sevent

异步语法的支持完全不依赖 asyncio,当然并没有说可以替代 asyncio 或者更好啥的,只是一种实现,如果有对异步 io 或者 python 异步语法实现感兴趣的可以一看吧。

只要是用于代理流量转发这样的场景,所以接口毕竟简单,当然支持范围也就没 asyncio 那么广了,从 echo 测试来看,性能还是要好于 asyncio 一些的,helpers 中也简单实现了几个工具。

HTTP 请求测试

import sevent

async def http_test():
    s = sevent.tcp.Socket()
    await s.connectof(('www.baidu.com', 80))
    await s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n')

    data = b''
    while True:
        try:
            data += (await s.recv()).read()
        except sevent.tcp.SocketClosed:
            break
    print(data.decode("utf-8"))
    await s.closeof()

sevent.run( http_test)

TCP 端口转发

import sys
import sevent

async def tcp_port_forward_server():
    server = sevent.tcp.Server()
    server.listen(("0.0.0.0", int(sys.argv[1])))

    while True:
        conn = await server.accept()
        pconn = sevent.tcp.Socket()
        pconn.connect((sys.argv[2], int(sys.argv[3])))
        conn.link(pconn)

sevent.run(tcp_port_forward_server)
4848 次点击
所在节点    Python
59 条回复
frostming
2021-09-08 14:38:19 +08:00
> 毕竟 python 的 async 和 await 语法可是在解释器层和 asyncio 耦合在一起的,异步 io 相关的实现就更多了,也不复杂

没有耦合啊,只要重新实现一套事件循环和异步 IO,就可以直接代替 asyncio,例子有:

- Trio: https://github.com/python-trio/trio by njs
- Curio: https://github.com/dabeaz/curio by David Beazely
sujin190
2021-09-08 15:50:48 +08:00
@frostming #41 这个的主要问题是,无论你用 async 方法 await 调用多少层,最终肯定会落到一个不是 async 或者 c 扩展的方法去,而这个方法是不能 await 的,如果你还需要等等 io 完成无法立刻得到结果,此时就需要有方法可以切换到其它协程去,而这个过程解释器层和 asyncio 耦合在一起了,还有 future 对象,set_result 后并没有直接触发 await,而是又走了一次 ioloop 循环,这个也直接写了就是走 asyncio
sujin190
2021-09-08 16:36:32 +08:00
@frostming #41 我看了下 trio 的实现,原理是调度器每 coroutine 迭代一步都需要重新压入调度器队列才能进行下一步,如果需要等待 io 就别压入调度器队列,这样调度器就可以切换到其它协程了,好吧,这也是一个方法,不过说起来 tornado 在 python2 上的实现其实也是这样的

但似乎 asyncio 的做法是直接切换栈帧的,对 coroutine 的每步迭代也是 c 直接写的,这两者效率差距有点大啊,asyncio 的 future 对象也是直接 c 写的
O5oz6z3
2021-09-08 21:30:29 +08:00
@ysc3839 #36 虽然我不是很懂 asyncio 和 C++,也不知道你的需求要怎么实现,但我猜你的 python 代码出错是因为你用了 asyncio.Future 。`Future(loop=None)` 要么默认绑定 asyncio 的事件循环,要么你传进去一个自定义 loop,这个 loop 的接口要兼容 asyncio 的 eventloop,大概像是 #41 楼说的那样?

而且 `async def test():...; test()` 本来也无法直接运行,大概需要这样运行 `asyncio.Task(test(), loop=None)`?

以下是个人理解。如果你要借用 async/await 语法糖,就要实现一个兼容 async-function/coroutine 的事件循环,也就是 asyncio 的工作,相当于 js 自带的事件循环。

虽然不知道要怎么实现 sync-callback 结合 async/await,个人理解就是 #19 楼那样,async 语法是用来等待 io 的,asyncio.sleep 就是模拟 io,所以也很好奇在没有 io 的情况下要怎么用 async 语法代替回调地狱。(顺便一提,上次在 /790207#90 的时候我的确说错了,py 协程可以是异步生成器,支持随意 suspend resume )
caicaiwoshishui
2021-09-08 22:45:15 +08:00
@O5oz6z3 歪个楼,想请教下大佬,在同步框架中比如 django,使用 asynico 时,这个异步协程是如何拿到主线程的事件循环的呀?
O5oz6z3
2021-09-09 00:06:20 +08:00
@caicaiwoshishui #45 问得好,这个问题,我也不会,有请真正的大佬指点……

@ysc3839 #36 补充一下,虽然不靠谱,稍微改了改糊了一个实现:
from asyncio import Future, Task, set_event_loop, AbstractEventLoop
from collections import UserList

class Loop(UserList, AbstractEventLoop):
...def call_soon(self, callback, *args, context=None):
......self.append(lambda: callback(*args))
...def get_debug(*_): pass
...__hash__ = lambda x:id(x)

callback_list = Loop()
set_event_loop(callback_list)
......
Task(test())
Task(test())
......
sujin190
2021-09-09 10:49:10 +08:00
@sujin190 #43 好吧,纠正一下,asyncio 也是每次 coroutine 每次迭代之后如果没用遇到 io 操作啥的需要等待就重新加入调度器队列,否则就在 io 完成后再重新加入调度器队列,这样调度器就可以在多个协程中相互切换了,并没用直接切换栈帧,那么确实不算是完全耦合在一起,其实似乎还是直接切换栈帧来的效率更高吧
sujin190
2021-09-09 10:55:21 +08:00
@caicaiwoshishui #45 asynico 的 ioloop 就是个 while True 死循环,所以如果你 django 运行在主线程,那么 asynico 就需要单独创建线程来运行了啊,而 asynico 的默认 get_event_loop 获取的是绑定到当前线程的,否则你就需要用个全局变量啥的保存这生成的 loop,这样就可以操作另外线程的 loop 了,再者 django 这种 web 程序又不是桌面 gui 程序,主线程哪里来的事件循环,asynico 似乎没有获取主线程事件循环的问题吧
caicaiwoshishui
2021-09-09 23:48:50 +08:00
@sujin190 感谢回答,不一定是拿到主事件循环,而是怎么在 django 的同步的主线程中切换到 asyncio 线程中?
sujin190
2021-09-10 10:22:41 +08:00
@caicaiwoshishui #49 call_soon_threadsafe 就可以在另外一个线程给 asynico 添加一个 callback 了,django 需要等等 callback 结果那就是多线程编程 Lock 、Event 、Semaphore 的东西了,写了个 flask 简单示例可以看看

https://gist.github.com/snower/b6d0288c60f4d40e544fb530a011ce62
caicaiwoshishui
2021-09-10 12:59:20 +08:00
@sujin190 感谢回答,清晰了很多
abersheeran
2021-10-12 10:59:14 +08:00
@sujin190 #15 我深入研究了一下,发现你说的这个不就是把 asyncio 重新实现了一遍吗?我以为切换过程是自动的,结果是手动的。那这个的意义在哪儿? greenlet 的效率比 generator 也高的有限吧。
abersheeran
2021-10-12 11:00:58 +08:00
async def async_call_method(func, *args, **kwargs):
ioloop = asyncio.get_running_loop()
future = ioloop.create_future()

def finish():
try:
result = func(*args, **kwargs)
if future._callbacks:
ioloop.call_soon(future.set_result, result)
else:
future.set_result(result)
except Exception as e:
if future._callbacks:
ioloop.call_soon(future.set_exception, e)
else:
future.set_exception(e)

child_gr = greenlet.greenlet(finish)
child_gr.switch()

return await future


我还试了你在 TorMySQL 里的函数,单纯用这个函数调用同步函数,比直接调用同步函数还慢一点。
sujin190
2021-10-12 11:31:06 +08:00
@abersheeran #53 我又没说比 asyncio 更好以做参考,单纯想了解 asyncio 实现可以看看,觉得有场景方便就用,激动个啥,关于比直接调用慢不很正常么,多加了几步操作肯定要花时间的啊
abersheeran
2021-10-12 14:07:00 +08:00
@sujin190 不是,我的意思是 TorMySQL 你确定是一个能有效使用的 MySQL 异步驱动吗?如果这个比直接调用同步函数还慢,那 TorMySQL 岂不是仅挂了一个空壳?
sujin190
2021-10-12 14:11:50 +08:00
@abersheeran #55 当然能用了,我们生产系统都跑好几年了,但是你这用异步函数和同步函数比是几个意思啊,TorMySQL 本来就是给 pymysql 套个壳让能在 asyncio 上使用异步 io,比同步直接请求慢本来就是正常的啊,你要比也应该和 aiomysql 比才对吧
abersheeran
2021-10-12 14:51:56 +08:00
@sujin190 啥?我上面说的是把那个函数包装一个同步 SQL 查询,然后丢进异步 Web 框架里测试的结果,还不如直接在异步框架里跑同步 SQL 查询快,如果用多线程包装的异步调用,那并发能达到四倍。

TorMySQL 我没试,我只有 pg 环境,所以我才问你,你确定这么写能真正的把同步 SQL 查询包装成异步的以提高并发吗?
abersheeran
2021-10-12 14:56:12 +08:00
我查到了 https://github.com/snower/TorMySQL/blob/master/tormysql/platform/asyncio.py#L83

原来内部还是异步的……只是用 greenlet 把 Coroutine 转成了 Greenlet 。
sujin190
2021-10-12 15:29:44 +08:00
@abersheeran #57 python 的异步本来就不快,事实上甚至有可能协程调度消耗了太多 cpu,然后你会发现 rps 还不如同步的呢,但是异步有异步的好处吧,提高并发倒是可以,提高 rps 就不能了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/800308

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX