“停止”一个线程?

简单来说,线程能停,很丑。Promise 没戏。

别去想“停止”一个线程了

Prelude

先前,我在用 Node 开发一些杂七杂八的东西时,曾经考虑过一个问题——“Promise 如何设置超时”?设置超时时间在异步任务、工作流中非常重要。

顺从直觉来看,这似乎是一个很简单的问题,就像是安装时点一下取消一样简单,找到封装好的方法调用就好。但是事情并没有这么简单。Node 中并没有这种方法,而 C++ 标准库中,也不存在停止线程的操作。看似很简单的操作,为什么大家都没能实现呢?

Attempt on Node

在 Node 中,我们将问题视为“Promise 如何在不修改内部代码的前提下设置超时”,因为 Node 中不存在多线程的观念 ((严格来说,有,但是很晦涩))。同时,为了能够得到开箱即用的设置超时能力,希望无需修改 promise 内部代码。

一种方法是,使用 Promise.race ((https://advancedweb.hu/how-to-add-timeout-to-a-promise-in-javascript/))。

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

Promise.race([new Promise(async (resolve, reject) => {
    for (let i = 0; i < 8; i++) {
        console.log(i);
        await new Promise(_ => setTimeout(_, 100))
    }
}), new Promise((resolve, reject) => {
    setTimeout(reject, 400)
})]).then(e => {
    console.log('Ok')
}).catch(e => {
    console.log('Err')
});

但 Promise.race 不能终止超时的 Promise。上面的代码运行结果会是这样:

0
1
2
3
Err
4
5
6
7

可以看到,Promise并没有在超时后停止运行,仍然将 8 个数字完整地输出。

有一些第三方库,如 p-timeout,也存在相同的问题。下面的代码会输出与 Promise.race 例子中形似的结果。

import * as pTimeout from "p-timeout";

pTimeout(new Promise(async (resolve, reject) => {
    for (let i = 0; i < 8; i++) {
        console.log(i);
        await new Promise(_ => setTimeout(_, 100))
    }
}), 400, "timeout").then(e => {
    console.log('ok', e);
}).catch(e => {
    console.log('err', e);
})

当涉及 IO,如磁盘读写、网络访问等,超时的 Promise 没有被终止还没那么糟(但是也很丑陋了)。如果是执行计算密集型的工作,可能整个程序就没法往下执行了。截止到目前,我还没有在 node 中找到一种优雅的解决方法。

这也可以理解,毕竟 Promsie 有段时间还是用 Generator 实现的,本质还是类似于状态机的顺序执行,想停也停不下来。

Attempt on C++

线程标准库里就那么几个成员函数,就差没把“没有”两个字写上去了。

但是还有一线希望——native_handle。native_handle 可以返回底层事先定义的线程句柄,对于 linux 来说,就是 pthread_t。linux 下 thread 标准库依赖 pthread,可以将 native_handle 取出,然后使用 pthread 相关的函数来操作。不过这样操作也使得代码与操作系统相关,并不是一件好事。

#include <bits/stdc++.h>

using namespace std;

int main() {

    thread th([]() {
      for (int i = 0;; i++) {
          cout << i << endl;
          this_thread::sleep_for(1s);
      }
    });

    auto nh = th.native_handle();
    this_thread::sleep_for(3s);

    if (int err = pthread_cancel(nh);err) {
        puts("pthread_cancel");
    }

    void *res;

    if (int err = pthread_join(nh, &res);err) {
        puts("pthread_cancel");
    }

    if (res == PTHREAD_CANCELED) {
        printf("main(): thread was canceled\n");
    } else {
        printf("main(): thread wasn't canceled (shouldn't happen!)\n");
    }

    this_thread::sleep_for(3s);
    puts("exit");

    exit(EXIT_SUCCESS);
}

运行结果为:

0
1
2
main(): thread was canceled
exit

Process finished with exit code 0

看起来似乎可以达到预期目的,但缺陷还是有的。

首先,这个操作依赖于操作系统的线程库。

其次,对于 linux,这个操作并没有保证立即结束线程。

A thread’s cancellation type, determined by pthread_setcanceltype(3), may be either asynchronous or deferred (the default for new threads). Asynchronous cancelability means that the thread can be canceled at any time (usually immediately, but the system does not guarantee this). Deferred cancelability means that cancellation will be delayed until the thread next calls a function that is a cancellation point. A list of functions that are or may be cancellation points is provided in pthreads(7).

asynchronous 取消,操作系统不做保证。deferred 取消当只执行计算操作时,可能很久都不会遇到 cancellation point。

pthread_kill 似乎也能杀掉线程,但是我没有成功。pthread_kill 也发送一个信号,但是线程没有捕获,直接杀掉了进程。有时间会再试一下。

Attempt on Java

Thread 类的 stop 等方法虽然存在,但被标记为 deprecated。原因在这篇文章写得很清楚,包括为什么不要停止一个线程。有时间会补充例子。

http://www.justdojava.com/2019/09/08/java-stopThread/

CC BY-NC-SA 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

2 thoughts on ““停止”一个线程?

  1. 关于Promise,之前开发的时候写的是,用Map之类的保存reject,想中断直接reject。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注