一次失败的类库开发经历

背景

这两周以来,我的所有开发上的精力全都花在了 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,在这个方法传入的异步函数内才是异步的上下文。

点此查看原文