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)
1
abersheeran 2021-09-07 10:26:05 +08:00
看到了 greenlet,我之前就有个想法,用 greenlet 把同步函数包装起来变成一个 awaitable 的对象,但是一直没空去做。你可以试试看做一下。
|
2
sujin190 OP @abersheeran #1 这个很简单,很早就搞过了
https://github.com/snower/TorMySQL 封装的 pymysql 可在 asyncio 下用 https://github.com/mongodb/motor 封装的 pymongo |
3
youngce 2021-09-07 11:02:59 +08:00
Twisted 的出现比 asyncio 还早,本质就是函数回调+事件循环。后来 Twisted 一部分核心贡献者转到 asyncio 去了,毕竟 asyncio 是未来
|
4
abersheeran 2021-09-07 11:08:14 +08:00
motor 用的线程池。你这个 TorMySQL 写的一言难尽……不过 https://github.com/snower/TorMySQL/blob/ad583aadc2844c4b4e32e948b1f3252582832022/tormysql/util.py 这个挺符合我之前的设想。测试结果怎么样?比 asyncio.to_thread 快吗?
|
5
sujin190 OP @youngce #3 你说的对,但是 Twisted 要能用 async 和 await 语法底层 ioloop 必须是 asyncio,我分享这个并没有说比 asyncio 更好,只是分享下对不使用 asyncio 的情况下如何使用 async 和 await 语法,感兴趣的话可以看看一看,毕竟 python 的 async 和 await 语法可是在解释器层和 asyncio 耦合在一起的,异步 io 相关的实现就更多了,也不复杂
而且吧其实 asyncio 为了使用更广,接口实现太复杂了,想简单搞个小工具啥的太麻烦了 |
6
Kilerd 2021-09-07 11:12:16 +08:00
|
7
sujin190 OP @abersheeran #4 motor 并没有用线程池,你没仔细看吧,greenlet 的切换肯定比线程切换快,款且还有同步锁的问题,实现要更复杂,性能肯定是 greenlet 更好了
TorMySQL 我们自己用了很久了,并没有啥问题啊,只是为了让在 python2 的 tornado 上也能正常运行,并不是全都是 python3 语法的 |
8
abersheeran 2021-09-07 11:22:11 +08:00
@sujin190 Motor 的线程池代码: https://github.com/mongodb/motor/blob/master/motor/frameworks/asyncio/__init__.py#L73
我还想问个 Greenlet 的问题:一个带系统中断操作的函数丢到 greenlet 里,它会自动在中断时 switch 到其他 greenlet 吗? |
9
sujin190 OP @abersheeran #8 好吧,我错了,没想到 0.5 版本之后这货就改成线程池了,只是不知道为啥这样改
应该不会吧,greenlet 切换的是栈帧,系统中断打断的是底层线程调用栈,python 的栈帧似乎是分配在堆上的,线程切换并不会影响 python 栈帧,自然也就不会导致 greenlet 切换了吧 |
10
abersheeran 2021-09-07 11:46:16 +08:00
@sujin190 中断不会导致 greenlet 切换?那你这个封装甚至不如 asyncio.to_thread 快啊。我明白 motor 怎么换成线程池了。看来我还得看看 gevent 去
|
11
sujin190 OP @abersheeran #8 如果是 signal 信号打断,signal 信号处理器结束的时候会恢复原来的栈帧,所以这种估计也不会导致 greenlet 切换吧
|
12
sujin190 OP @abersheeran #10 你说的是系统 io 会不会导致 greenlet 切换吧?这个系统 io 处理都是运行在主 greenlet 里的,是你需要写数据读数据的时候,你主动切换的主 greenlet 去,不像线程池一样当系统 io 产生的时候由操作系统调度线程切换
|
13
ysc3839 2021-09-07 12:06:50 +08:00 via Android
借楼问一下,Python 里 async function 能否当成回调函数使用?
比如说已经有一个队列了,在 await 某个对象时得到一个回调函数加入队列,后续执行这个函数恢复 async function 执行。 |
14
abersheeran 2021-09-07 12:09:25 +08:00
@sujin190 那只要一个 SQL 查询卡在那儿,后续所有 SQL 查询不都被阻塞了吗?
|
15
sujin190 OP @abersheeran #14 不啊,主 greenlet 使用 epoll 可以同时处理很多个连接的,连接 io 事件产生的时候主 greenlet 处理完事件后会依次切换到对应的子 greenlet 做业务处理,子 greenlet 又可以产生更多的 io 操作主动切回主 greenlet 处理了啊,这样不就异步并行可以同时处理无数个 sql 查询请求了
|
16
abersheeran 2021-09-07 12:30:40 +08:00
@sujin190 😦好的我去看看
|
17
sujin190 OP @ysc3839 应该是可以的吧,只不过普通队列并不能让这个函数运行起来,你需要从队列取出来后扔到 asyncio 中去运行起来
|
19
sujin190 OP @ysc3839 #18 也是可以的,只是不用 asyncio 这样的异步 io,那么异步方法似乎没啥用了吧,没啥用的必要了
|
20
tmac010sjh 2021-09-07 15:19:11 +08:00
苏神依旧这么牛逼,还在 mg 么?
|
21
wamson 2021-09-07 15:25:50 +08:00
正好对这个有兴趣,话说这个有什么比较有名的方案吗?比如底层是 c++,io_loop 是 lib_event,通过 pybind11 来驱动 python 脚本层级,那么 python 层该怎么使用 async 语法糖呢。眼馋 python 的 async 语法糖好久了。
|
22
haoliang 2021-09-07 15:56:43 +08:00
没有文档,粗略爬过代码;我的理解中,`sevent.go(callback)` 对应 `asyncio.create_task`, 在 sevent 中如何等待这个 callback 的完成呢? callback 运行过程中抛错,该如何处理?
|
23
sujin190 OP @wamson #21 async 语法糖本质就是一个迭代器,用迭代器的方法驱动运行就好了,搞 c++的话,去瞅一下 asyncio 的源码很快就知道了吧,而且你可以把 asyncio 的那部分源码搬过来放到你的 c++代码里啊
|
24
sujin190 OP @haoliang #22 只是一看,没啥动力写文档啊,callback 出错只是单纯用 logging 输出调用栈信息了,其实一般来说如果你关注 callback 出的错,那么你应该在一进入 callback 时就自己加 try 就是了呀
|
25
sujin190 OP @tmac010sjh #20 跑人了
|
27
sujin190 OP @ysc3839 #26 没有底层的 asyncio 的 ioloop,协程之间就缺少切换点了,难道你想用线程池配合 await 使用?可是多线程模式下 Lock 、Event 、Semaphore 就是用来解决你遇到的回调问题,并不需要多次一举强行又别扭的使用 await 啊
await 本质就是个状态管理器,他也是注册 callback,只不过这个 callback 用来触发协程切换了,但是线程调度由操作系统来管理,所以这不又回到 Lock 、Event 、Semaphore 了 |
28
ysc3839 2021-09-07 17:32:29 +08:00 via Android
@sujin190 我就是为了用 async function 代替回调函数,并不是要用什么线程池,整个程序是单线程的,也不希望搞成多线程的模式。
这也是我不能理解 Python 协程的地方,别的语言如 C++, JavaScript 的协程就是回调函数的语法糖,但 Python 好像要有个调度器才能跑。 |
29
sujin190 OP @ysc3839 #28 JavaScript 语言直接自带调度器,你都不能不要,不知道你用的 C++的库是啥,其实你这个想法才挺奇怪,async await 语法本来就是 A 等待 B 的一个结果,本身就代表有两个独立调用栈,没用调度器,直接一个单一线程如何能驱动两个独立调用栈运行呢
|
30
ysc3839 2021-09-07 17:48:31 +08:00 via Android
@sujin190 但是 JavaScript 的调度器跟它的协程关系并不大,调度器执行的是回调函数,并不跟协程深度绑定。
C++ 说的是 C++20 内置的协程。 没有调度器,也能直接通过回调函数来驱动协程执行。比如 C++ 的协程在 await 的时候,被 await 的对象能拿到协程的“回调函数”,后续执行这个回调函数就是恢复协程执行了。 |
31
sujin190 OP @ysc3839 #30 我知道你说的是啥,其实就是 future 对象吧,需要传 callback 的地方你可以构造一个 future 对象传过去,future 的 set_result 就是你说的 c++中的那个能拿到协程的“回调函数”了,Javascript 的异步函数返回时一个 Promise 对象,换言之,你需要传 callback 也可以改成传 Promise 对象,Javascript 不需要显示把协程放在调度器中运行时因为自动在全局包了一个调度器函数,其实底层和 python 是一样的
|
32
sujin190 OP @ysc3839 #30 关于 C++协程据我之前看过几个库的实现来看,c++作为底层语言,自身就能够通过操作内存结构和长 jmp 指令改变调用栈,所以基于此是可以无需调度器就可以完成协程切换的,但是无论 Javascript 、python 、ruby 、php 这样的语言可是都做不到手动修改栈帧这样的操作,这点和 c++这样的更底层的语言还是很大不同的,Javascript 看起来有点像,也只是解释器会隐式加很多东西
|
33
ysc3839 2021-09-07 19:01:32 +08:00
我说的“协程的回调函数”不是 future 或者 JS 异步函数返回的 Promise,而是用于恢复协程执行的东西,类似 Promise 里面的 resolve 函数。
我说的 C++ 协程不是第三方实现的有栈协程,而是语言本身实现的无栈协程,这种协程不依赖具体硬件平台,而是依赖编译器实现。 可以看一下用 Babel 转换过的协程代码,或者反编译看看 C++ 协程编译后的代码,其实就是在 await 的地方拆分开。 用伪代码举个例子: 原始代码: ``` async function test() { func_a(); await func_b(); await func_c(); func_d(); } test(); ``` 处理后的代码: ``` function test(state) { switch(state.step) { case 0: state.step = 1; func_a(); func_b(test, state); break; case 1: state.step = 2; func_c(test, state); break; case 2: func_d(); break; } } let state = {step: 0}; test(state); ``` 此时 func_b 或者 func_c 要恢复协程执行的话,只需要调用 test(state); 即可,这样可以轻松对接那些使用回调函数的库,避免回调地狱。 印象中 Python 也是类似这种模式的协程,按理说可以直接当回调函数用,但是实际上却捆绑了调度器,反而丧失了灵活性。 就像前面 @wamson 所说的,很多情况下是已经有了一个现成的使用回调函数的事件循环,想要使用 async await 的语法简化代码,这种情况下 C++ 或 JS 的协程很容易就能对接,而 Python 就做不到了。 |
34
joApioVVx4M4X6Rf 2021-09-07 19:31:12 +08:00
牛逼啊,好久没在 python 区看见这种高端技术贴了,求多发点
|
35
sujin190 OP @ysc3839 #33 你这是把 c++和 JavaScript es5 的实现想完全搬到 python 上来啊,但事实上这两都是没有更好方法的时候不得已才这样用的,python 的实现更简单,用 future 对象把那些用 callback 的库包装一下就行了
``` def func(callback): callback() def func_await(): futuer = Futuer() def on_finish(): futuer.set_result() a(on_finish) return futuer async def test(): func1() await func_await() await func_await2() func2() ``` 大概就是这样样子就好了 |
36
ysc3839 2021-09-08 10:27:06 +08:00
@sujin190 自己写了段测试代码,但是运行时出错了
https://gist.github.com/ysc3839/4445c6c2bc1e0ce02790a52fed0c8b38 错误信息是: test.py:20: RuntimeWarning: coroutine 'test' was never awaited test() RuntimeWarning: Enable tracemalloc to get the object allocation traceback test.py:21: RuntimeWarning: coroutine 'test' was never awaited test() RuntimeWarning: Enable tracemalloc to get the object allocation traceback |
37
ysc3839 2021-09-08 10:48:41 +08:00
@ysc3839 顺便写了个用 C++ 协程实现的 https://godbolt.org/z/sGnzTnvjr
|
38
sujin190 OP @ysc3839 #36 https://gist.github.com/snower/c4f1d4d55281e9456daf1a66831db821
async 函数调用后其实返回了一个 coroutine 对象,python 默认不会启动调度器,所以默认并不会运行这个 coroutine 对象,JavaScript 可以,那是因为 JavaScript 默认就启动了调度器,全局定义为被执行的 Promise 都会默认被直接执行,这两个没啥区别 当然 python async 函数调用返回 coroutine 使用 send 函数就能手动运行起来了,但是注意 asyncio 的 Future 调用 set_result 触发 await 返回其实是又被放到当前 asyncio loop 中去执行了,所以你还是要用 asyncio 创建 loop 来运行 我看 c++ 20 的协程也是需要调度器的吧 |
39
sujin190 OP @ysc3839 #37 静态语言改编译器,改内存布局能,直接长 jmp 就是牛逼,这是在编译时直接把 test 函数拆成几个了啊
|
41
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 |
42
sujin190 OP @frostming #41 这个的主要问题是,无论你用 async 方法 await 调用多少层,最终肯定会落到一个不是 async 或者 c 扩展的方法去,而这个方法是不能 await 的,如果你还需要等等 io 完成无法立刻得到结果,此时就需要有方法可以切换到其它协程去,而这个过程解释器层和 asyncio 耦合在一起了,还有 future 对象,set_result 后并没有直接触发 await,而是又走了一次 ioloop 循环,这个也直接写了就是走 asyncio
|
43
sujin190 OP @frostming #41 我看了下 trio 的实现,原理是调度器每 coroutine 迭代一步都需要重新压入调度器队列才能进行下一步,如果需要等待 io 就别压入调度器队列,这样调度器就可以切换到其它协程了,好吧,这也是一个方法,不过说起来 tornado 在 python2 上的实现其实也是这样的
但似乎 asyncio 的做法是直接切换栈帧的,对 coroutine 的每步迭代也是 c 直接写的,这两者效率差距有点大啊,asyncio 的 future 对象也是直接 c 写的 |
44
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 ) |
45
caicaiwoshishui 2021-09-08 22:45:15 +08:00
@O5oz6z3 歪个楼,想请教下大佬,在同步框架中比如 django,使用 asynico 时,这个异步协程是如何拿到主线程的事件循环的呀?
|
46
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()) ...... |
47
sujin190 OP @sujin190 #43 好吧,纠正一下,asyncio 也是每次 coroutine 每次迭代之后如果没用遇到 io 操作啥的需要等待就重新加入调度器队列,否则就在 io 完成后再重新加入调度器队列,这样调度器就可以在多个协程中相互切换了,并没用直接切换栈帧,那么确实不算是完全耦合在一起,其实似乎还是直接切换栈帧来的效率更高吧
|
48
sujin190 OP @caicaiwoshishui #45 asynico 的 ioloop 就是个 while True 死循环,所以如果你 django 运行在主线程,那么 asynico 就需要单独创建线程来运行了啊,而 asynico 的默认 get_event_loop 获取的是绑定到当前线程的,否则你就需要用个全局变量啥的保存这生成的 loop,这样就可以操作另外线程的 loop 了,再者 django 这种 web 程序又不是桌面 gui 程序,主线程哪里来的事件循环,asynico 似乎没有获取主线程事件循环的问题吧
|
49
caicaiwoshishui 2021-09-09 23:48:50 +08:00 via iPhone
@sujin190 感谢回答,不一定是拿到主事件循环,而是怎么在 django 的同步的主线程中切换到 asyncio 线程中?
|
50
sujin190 OP @caicaiwoshishui #49 call_soon_threadsafe 就可以在另外一个线程给 asynico 添加一个 callback 了,django 需要等等 callback 结果那就是多线程编程 Lock 、Event 、Semaphore 的东西了,写了个 flask 简单示例可以看看
https://gist.github.com/snower/b6d0288c60f4d40e544fb530a011ce62 |
51
caicaiwoshishui 2021-09-10 12:59:20 +08:00 via iPhone
@sujin190 感谢回答,清晰了很多
|
52
abersheeran 2021-10-12 10:59:14 +08:00
@sujin190 #15 我深入研究了一下,发现你说的这个不就是把 asyncio 重新实现了一遍吗?我以为切换过程是自动的,结果是手动的。那这个的意义在哪儿? greenlet 的效率比 generator 也高的有限吧。
|
53
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 里的函数,单纯用这个函数调用同步函数,比直接调用同步函数还慢一点。 |
54
sujin190 OP @abersheeran #53 我又没说比 asyncio 更好以做参考,单纯想了解 asyncio 实现可以看看,觉得有场景方便就用,激动个啥,关于比直接调用慢不很正常么,多加了几步操作肯定要花时间的啊
|
55
abersheeran 2021-10-12 14:07:00 +08:00
@sujin190 不是,我的意思是 TorMySQL 你确定是一个能有效使用的 MySQL 异步驱动吗?如果这个比直接调用同步函数还慢,那 TorMySQL 岂不是仅挂了一个空壳?
|
56
sujin190 OP @abersheeran #55 当然能用了,我们生产系统都跑好几年了,但是你这用异步函数和同步函数比是几个意思啊,TorMySQL 本来就是给 pymysql 套个壳让能在 asyncio 上使用异步 io,比同步直接请求慢本来就是正常的啊,你要比也应该和 aiomysql 比才对吧
|
57
abersheeran 2021-10-12 14:51:56 +08:00
@sujin190 啥?我上面说的是把那个函数包装一个同步 SQL 查询,然后丢进异步 Web 框架里测试的结果,还不如直接在异步框架里跑同步 SQL 查询快,如果用多线程包装的异步调用,那并发能达到四倍。
TorMySQL 我没试,我只有 pg 环境,所以我才问你,你确定这么写能真正的把同步 SQL 查询包装成异步的以提高并发吗? |
58
abersheeran 2021-10-12 14:56:12 +08:00
我查到了 https://github.com/snower/TorMySQL/blob/master/tormysql/platform/asyncio.py#L83
原来内部还是异步的……只是用 greenlet 把 Coroutine 转成了 Greenlet 。 |
59
sujin190 OP @abersheeran #57 python 的异步本来就不快,事实上甚至有可能协程调度消耗了太多 cpu,然后你会发现 rps 还不如同步的呢,但是异步有异步的好处吧,提高并发倒是可以,提高 rps 就不能了
|