一次失败的类库开发经历
背景
这两周以来,我的所有开发上的精力全都花在了 PyQWebWindow 这个库的 IPC 模块。
我从开始开发时就定好了目标:最终要实现类似于 Electron 中 IpcMain 和 IpcRenderer 的效果。即,允许用户在主进程和渲染进程中通过 .on
函数来监听彼此进程发出的事件,也能通过 .emit
来向彼此提交事件。两个进程间的通信应该是完全异步的,不能让监听阻塞各自进程的主线程。
但是我忽视了编程语言间的区别。Electron 之所以会采用事件驱动的模式,很大一部分原因是由于 Node.js 本身对于异步编程和事件驱动的模式具备更完善的支持。
具体来说
举个最简单的例子,如果我在 Node.js 中定义了一个异步函数,
async function test() {
setTimeout(() => {
console.log("test")
}, 1000)
}
我可以直接通过函数调用的方式异步地调用这个函数,让解释器在主线程自动空闲时执行这个函数
test()
而在 Python 中,我同样可以定义一个异步函数,
async def test():
await asyncio.sleep(1)
print("test")
但在 Python 中,如果只是直接执行这个函数,是不会有任何效果的
test()
# returns: `<coroutine object test at 0x0000024858A33140>`
我必须使用 asyncio.run
来调用这个函数,这个函数内的语句才会被实际调用:
asyncio.run(test()) # 这会阻塞主线程
在 Node.js 中,执行一个异步函数底层上是创建了一个异步任务,并将这个任务推进全局的任务栈中,其实 Python 中也是支持这样的操作的:
asyncio.create_task(test()) # 这会自动创建异步任务并推入全局的任务栈
但问题在于,asyncio.create_task
方法必须在 asyncio.run
方法创建的异步事件循环中执行才能正常工作,这就绕回了前面提到的问题了。
底层原因
这个失败实际上是由于 Python 和 Node.js 两个编程语言的底层设计差异。
在 Node.js 中,顶层作用域可以被视为一个异步上下文,也因此,Node.js 是支持顶层 await 的。
而在 Python 中,顶层作用域就是一个普通的同步上下文,用户需要调用 asyncio.run
,在这个方法传入的异步函数内才是异步的上下文。