【问题标题】:Terminate a thread which is running a native code终止正在运行本机代码的线程
【发布时间】:2012-02-04 10:32:31
【问题描述】:

在我的应用程序中,我有一个通过 JNI 桥调用的本机代码的包装器。此本机代码需要在单独的线程中执行(并行处理)。但是问题是代码有时会“挂起”,因此需要“强制”终止线程。不幸的是,我还没有找到任何“微妙”的方法来做到这一点:一般建议是告诉线程中的代码优雅地退出,但我不能用这个本机代码(上面都是第 3 方代码)来做到这一点。

我使用 Java Concurrent API 进行任务提交:

Future<Integer> processFuture = taskExecutor.submit(callable);

try {
    result = processFuture.get(this.executionTimeout, TimeUnit.SECONDS).intValue();
}
catch (TimeoutException e) {
    // How to kill the thread here?
    throw new ExecutionTimeoutException("Execution timed out (max " + this.executionTimeout / 60 + "min)");
}
catch (...) {
    ... exception handling for other cases
}

Future#cancel() 只会中断线程,但不会终止它。所以我使用了以下技巧:

class DestroyableCallable implements Callable<Integer> {

    private Thread  workerThread;

    @Override
    public Integer call() {
        workerThread = Thread.currentThread();

        return Integer.valueOf(JniBridge.process(...));
    }

    public void stopWorkerThread() {
        if (workerThread != null) {
            workerThread.stop();
        }
    }
}

DestroyableCallable callable = new DestroyableCallable();

Future<Integer> processFuture = taskExecutor.submit(callable);

try {
    result = processFuture.get(this.executionTimeout, TimeUnit.SECONDS).intValue();
}
catch (TimeoutException e) {
    processFuture.cancel(true);
    // Dirty:
    callable.stopWorkerThread();

    ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) taskExecutor;

    logger.debug("poolSize: " + threadPoolTaskExecutor.getPoolSize() + ", maxPoolSize:"
                    + threadPoolTaskExecutor.getMaxPoolSize() + ", activeCount:"
                    + threadPoolTaskExecutor.getActiveCount());
    }

    throw new ...;
}
catch (...) {
    ... exception handling for other cases
}

此代码的问题/问题:

  • 一般情况下这样做是否正确?还有其他更优雅的选择吗?
  • 任务执行器上的activeCount 没有减少,因此任务执行器仍然“认为”线程正在运行
  • 我不得不将workerThread != null check 添加到stopWorkerThread() 方法中,因为在某些情况下这个变量是null。我不明白这些情况是什么......

注意事项:

  • 本机代码不使用文件描述符(套接字)。一切都作为数据块传递给它,并以相同的方式返回。
  • 本机代码占用大量 CPU。即使它保证终止,也可能需要很长时间。

赏金编辑:重新访问本机代码的方法/建议很明确,请不要在回复中提供。我需要纯 Java 解决方案/解决方法。

【问题讨论】:

    标签: java multithreading


    【解决方案1】:

    Java 有强制线程终止的纯选项。它只是古老且已弃用的 Thread.stop() (AFAIK)。和 no 选项用于 safe 线程终止(这就是为什么 .stop() 被弃用,甚至允许 JVM 实现者不实现)。

    原因是应用程序内的所有线程共享内存和资源——因此,如果您在某个任意点强制终止线程,您无法确定哪些终止的线程不会离开一些处于不一致状态的共享内存/资源。而且您甚至不能(通常)假设 哪些 资源(可能)是脏的(因为您不确切知道线程在哪一点停止了)。

    因此,如果您希望应用程序的某些线程能够中断,唯一的解决方案是在设计阶段提供一些“保存点”符号 - 目标线程代码中的位置,这些位置保证不会改变共享状态,因此线程在这里退出是安全的。这正是 Thread.stop() javadocs 告诉您的:安全中断线程的唯一方法是设计线程的代码,以便它可以自己响应某种中断请求。某种标志,由线程不时检查。

    我试图告诉你:你不能做你被问到的关于使用 java 线程/并发的事情。我可能会建议你的方式(这里很早就给出了)是在单独的过程中完成你的工作。强行杀死进程比线程安全得多,因为 1)进程彼此之间更加分离,2)操作系统在进程终止后负责许多清理工作。杀死过程并不完全安全,因为存在某种资源(例如文件),默认情况下操作系统不会清除这些资源,但在您的情况下,它似乎是安全的。

    因此,您设计了小型独立应用程序(甚至可能在 java 中——如果您的第三方库不提供其他绑定,甚至在 shell 脚本中),唯一的工作就是让您进行计算。你从主应用程序开始这样的过程,给它工作,然后启动看门狗。它看门狗检测到超时——它强制终止进程。

    这是唯一的解决方案草案。你可以实现某种进程池,如果你想提高性能(启动进程可能需要时间)等等......

    【讨论】:

    • 感谢公平通知。对您的帖子的一个小评论:由于启动的线程正在执行本机代码,因此没有与 Java 部分共享的锁(除非您通过 JNI 显式编码)。所以唯一有风险的资源是打开的文件描述符(这对我来说不是这种情况,因为所有必要的数据都作为 blob 传递并作为字符串取回)。唯一的问题是如何告诉ThreadPoolTaskExecutor 线程真的死了?我查看了代码:它应该真的能捕捉到这种情况。
    • 没有锁——好的,可能是。但是本机代码中的内存泄漏呢?你能确定原生库的设计和编码足够安全,可以优雅地处理异常终止吗?它是否释放所有退出路径上分配的内存,包括异常路径?你将如何处理内存,在本机代码中泄漏?关于一些操作系统级资源的同样问题——如果本机库分配内核级锁怎么办?启动几个额外的线程?
    • 我同意堆上的内存泄漏(如threads share the same heap)。我不是专家,但我认为用户线程不能在内核模式下被中断,否则杀死线程会损坏例如文件系统,以防线程在写入磁盘时被杀死。所以未释放的内核级锁将是非常可悲的。其余的我完全支持你的立场:过程更安全。 JNI 桥的更多内容是我从独立进程中制作的,因此无需努力恢复。
    • 我认为最合适的解决方案是实现像 Apache 那样的东西:主进程应该启动几个子进程(例如 2 个),每个子进程都应该处理 X 请求并死掉(它将由主进程)。
    【解决方案2】:

    您可以将对 JNI 方法的单个调用包装到一个单独的 Java 应用程序中,然后使用 java.lang.Process 派生另一个 Java 进程。然后你可以调用Process.destroy()在操作系统级别销毁该进程。

    根据您的环境和其他考虑因素,您可能需要采取一些技巧来找出如何找到 java 可执行文件,尤其是当您正在构建一些可以在不同平台上运行的可再发行软件时。另一个问题是 IPC,但可以使用 Process 的输入/输出流来完成。

    【讨论】:

    • 强行杀死一个进程甚至不如停止一个线程优雅。
    • 强行杀死一个进程是安全的(操作系统会处理它)——这对于线程来说是不正确的。所以它更优雅的解决方案,我想它是唯一的解决方案,如果已知第三方 JNI 库不会产生一些外部资源,比如文件,这些资源不会被操作系统清除在进程终止时。
    【解决方案3】:

    你这里绝对是一个丑陋的黑客......

    首先,线程池线程并不意味着单独调整,通常应该让其运行直到完成,尤其是不要用Thread.stop() 停止,即使是普通线程也不建议这样做。

    正如我所说,使用Thread.stop() 是不被鼓励的,并且通常会使线程处于不一致的状态,这可能是线程池没有将线程视为“死”的原因。它甚至可能根本不会杀死它。

    知道为什么本机代码会挂起吗?我认为您的问题的根源在这里,而不是线程停止部分。线程通常应尽可能运行直到完成。也许你可以找到一个更好的实现,它可以正常工作(或者如果你编写了它,可以实现一些不同的东西)。

    编辑:至于第 3 点,您可能需要将对当前线程的引用声明为 volatile,因为您在一个线程中分配它并在另一个线程中读取它:

    private volatile Thread workerThread;
    

    编辑 2:我开始意识到您的 JNI 代码只进行数值计算,并且不会打开任何可能保持不一致状态的句柄,以防线程被突然终止。你能确认一下吗?

    在这种情况下,请允许我违背我自己的建议并告诉您,在这种情况下,您可以安全地使用Thread.stop() 终止线程。但是,我确实建议您使用单独的线程而不是线程池线程,以避免使线程池处于不一致的状态(正如您所提到的,它不会将线程视为已死)。它也更实用,因为您不必执行所有这些技巧来让线程自行停止,因为您可以直接从主线程调用stop(),这与线程池线程不同。

    【讨论】:

    • Tudor,感谢您指出第三点,为此 +1。是的,我知道本机代码“挂起”的原因:数据处理时间太长,在这种情况下,我希望跳过数据(在我的情况下,为了处理速度而丢失一些数据是可以的)。您的建议是查看本机代码。 除了还有什么?
    • @dma_k:你能保证原生代码最终会终止吗?
    • 是的,我有保证。但这可能需要几天时间:)
    • @dma_k: 所以在超时后忽略线程并让它自己完成并不是一个真正的选择
    • 我很高兴,但是线程会占用 CPU,因为计算量很大。
    【解决方案4】:

    由于您正在处理第 3 方代码,我建议您创建一个本地 shell 应用程序来处理调用、跟踪和终止这些线程。如果您的许可协议提供任何形式的支持,最好让这个第 3 方为您做这件事。

    http://java.sun.com/docs/books/jni/html/other.html

    【讨论】:

    • 如果我理解正确的话,你的意思是Java Threading投降来处理这种情况?
    【解决方案5】:

    我不会重复 Tudor 给出的所有宝贵建议...... 我将在使用任何类似队列的机制来处理主 Java 应用程序和启动的本机线程之间的通信时添加您的替代架构点...... 该线程可能是代理的客户端,如果发生某些特殊事件(终止)并采取相应措施(停止长时间运行的作业),则会收到通知 当然,这会增加一些复杂性,但这是一个非常优雅的解决方案.. 当然,如果本机线程不健壮,它不会改变任何整体健壮性.. 处理本机线程和代理之间通信的一种方法是使用类似 STOMP 的接口(许多代理 Apache activemq、来自 Oracle 的 MQ 公开此类接口)...

    HTH 杰罗姆

    【讨论】:

    • 这个代理应该用Java编写吗?如果是,您能否为其提供元逻辑?根据您所说,代理应该在另一个单独的线程中运行来处理事件......
    猜你喜欢
    • 2011-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-03
    • 2017-07-30
    • 2018-08-12
    相关资源
    最近更新 更多