写了一个有趣的 JS/TS 的任务管理库,可以帮助您中止异步函数/Promise 运行

103 天前
 pursuer

写过 JS 的都知道 JS/TS 的异步方案采用 async,await,Promise 的无栈协程方案。Python 也是采用的类似方案,但 Python 提供更多的控制包括 cancel 一个 task ,可以尝试提前结束一个异步任务,同时 JS/TS 也没有 current_task 或者类似 Java 的 ThreadLocal 获取上下文变量的方案。

通常的实现中断执行的写法是手动判断类似 AbortSignal.throwIfAborted 的方案,但写起来没那么舒心。

于是我想到是否可以通过改写 Promise.then 实现自动中断 await 执行?实际写下来以后的发现居然真的可以,于是有了下面的这个库。

https://github.com/partic2/protask

https://gitee.com/partic/protask

使用示例:


import {task} from 'protask'

function sleep<T>(milliSeconds: number, arg?: T): Promise<T> {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, milliSeconds, arg)
    });
}

async function printTaskLocal(){
    await sleep(100);
    console.info(task.locals());
}

task.create('test task 1',async ()=>{
    try{
        for(let i=0;i<100;i++){
            task.locals()!.count=i;
            await printTaskLocal();
            await sleep(1000)
        }
    }catch(e:any){
        console.info(e.toString());
    }
});

task.create('test task 2',(async ()=>{
    await sleep(3000);
    console.info('abort task 1')
    task.abort('test task 1');
}));

上面的代码创建了一个 task1 然后在 task2 中中断了 task1 的运行。同时在 task1 中打印 task local 。

目前只是一个非常简单实验性质的库,并未完善测试过,请注意使用风险。

觉得有意思的话可以点个 star ,万一以后就进 tc39 了呢?(笑

2948 次点击
所在节点    分享创造
29 条回复
rppig42
103 天前
👍

有正经这个需求的可以了解下 RxJS
nomagick
103 天前
不是这样的,你这没有作用。。

主动中断同步代码目前只能通过操作系统,中断线程或中断进程
主动中断异步代码,可以通过 iterator 在 yield 的节点中断,但在 js 语法之外需要魔改

你这只是在上游 then 之后选择是否往下游返回,没有中断任何代码的执行,掩耳盗铃了属于是
pursuer
103 天前
@nomagick 实现 Python 的 cancel 类似的机制,中断异步传递抛出异常,同步代码都是没法中断的,确实像你说的可以魔改为 await 为 iterator 模式, 但我写的这个方法可以不用魔改 js 就可以实现这个效果。
dapang1221
103 天前
啊?你们前端终于把浏览器搞成了操作系统了吗
nomagick
103 天前
@pursuer 不一样,通过 iterator 实现代码是中断了的,运行时知道现在代码已经 throw 或者 return ,但你现在这样,通过 hack 阻止 Promise 结算,Promise 是一直吊在 pending 状态的,运行时也不知道你这部分代码不会再执行了,只知道 Proimse 没有 resolve 。

具体运行时有没有足够聪明能够解开这个泄漏局我不太了解,总之你这操作非常危险,很有可能解不开,而且即便能解开,我看你代码一旦 cancel, 因为你阻断了 Promise 结算,所以 cleanup 的步骤就永远不会执行,但你对这些 AbortSignal 却有全局的引用,这部分也会泄露。
pursuer
103 天前
@nomagick Promise 不会 pending ,abort 的情况会直接传递到 onrejected ,抛出 AbortError 。在 taskMain 函数返回后会做 task 的清理工作。当然,如果 taskMain 返回后有其他继承同一 task 的 Promise 尝试访问 task 上下文会得到 undefined ,这算是一个小问题。
nomagick
103 天前
@pursuer 所以你至少在 cancel 之后需要 reject cancel error, 这样下游的代码路径才能继续结算,所以下游也需要再在某个地方 catch cancel error ,对代码的入侵性不亚于显式 if (await jobCancelled) return;

说到底对代码执行流程的操作,还是要交给语言和运行时层面去解决,如果一个函数流程,是不是被完整执行,还可以被外部代码莫名其妙地影响,这对整个系统来讲完全就是一个灾难
nomagick
103 天前
@pursuer 没有啊,在 cancel 的情况,不是 reject 的情况 https://github.com/partic2/protask/blob/b22d446a33cf47e34f3aa4e6d4244185aa75d9cf/src/index.ts#L57-L60
你在这 catch 了之后没有做任何操作,Promise 就吊在这了,这也是你能看起来中断执行的所在
nomagick
103 天前
@nomagick 哦,对不起,是我搞混了
pursuer
103 天前
@nomagick 这个方法确实比较 hacky ,里面也可能埋藏着尚未发现的坑,所以我也指明这是一个实验性的项目。只是有时候确实想要个这样的控制 async/await 运行流的工具,不知道 tc39 以后会不会搬出类似的东西。
nomagick
103 天前
@pursuer 打扰了,看起来真的可以,进入了我的知识盲区,原来复写 promise.then 就能中断执行流,那我就比较好奇了,async function 原本的那个 promise 后来怎么样了,这个 promise 是不是吊起来了
nomagick
103 天前
不对,我真的下载下来运行了。

复写 then 不足以中断执行流,你的 example 之所以能够 work 是因为 `task.locals()!.count=i;` 这句在 cancel 之后抛了异常,起到了 abort error 的效果。。。
nomagick
103 天前
```typescript
import { task } from 'protask';

function sleep<T>(milliSeconds: number, arg?: T): Promise<T> {
return new Promise(function (resolve, reject) {
setTimeout(resolve, milliSeconds, arg);
});
}

async function printTaskLocal() {
await sleep(100);
console.info(task.locals());
}

task.create('test task 1', async () => {
try {
for (let i = 0; i < 100; i++) {
// task.locals().count = i;
console.log('task 1 running');
await printTaskLocal();
await sleep(1000);
}
console.log('Task 1 resolve');
} catch (e: any) {
console.log('Task 1 error');
console.info(e.toString());
} finally {
console.log('Task 1 finally');
}
});

task.create('test task 2', (async () => {
await sleep(3000);
console.info('abort task 1');
task.abort('test task 1');
}));
```
pursuer
103 天前
因为我是用了 super.then 的,所以原本 promise 的内部处理应该遵照原有的实现,只是在 onfulfilled 前检测中止状态,转为调用 onrejected
pursuer
103 天前
@nomagick 抛出异常符合预期的,Python 也是抛出 CancelError ,只有抛出异常才能保证类似 try{}finally 的资源正常释放。
nomagick
103 天前
不啊,你这言之凿凿的都给我整不自信了,你这真的是没有中断任何代码的执行,而是你的 cancel 操作,造成了 task.locals.count 的赋值失败,这产生了
TypeError: Cannot set properties of undefined (setting 'count')

这才中断了执行,如果你不操作 task.locals ,task1 就会一直运行下去
pursuer
103 天前
@nomagick 我这边有 node 和 Chrome 测试是正常的,输出是
{ count: 0 }
{ count: 1 }
{ count: 2 }
abort task 1
AbortError: This operation was aborted
不知道你那边用的什么运行时,可能哪里还有瑕疵,试试看 task.locals.count 赋值删除能不能抛出异常?
nomagick
103 天前
我这边是 node.js 20.11.1
我觉得可能和原生 async function 有关,如果是原生的 async function 你这个 hack 就中断不了,但如果是 babel 之类的编译器模拟出来的,打断 then 链条就可以。
9ki
103 天前
pursuer
103 天前
@nomagick
使用同一版本依然无法复现问题。。。用的 windows x64 ,运行你给的代码如下

sh-3.1$ node -v
v20.11.1
sh-3.1$ npm run build && npm run test

> npmtest@1.0.0 build
> tsc


> npmtest@1.0.0 test
> node dist/index.js

task 1 running
{}
task 1 running
{}
task 1 running
{}
abort task 1
Task 1 error
AbortError: This operation was aborted
Task 1 finally

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

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

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

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

© 2021 V2EX