终止运行本机代码的线程

9
在我的应用程序中,我有一个包装器覆盖了一些通过JNI桥调用的本地代码。这些本地代码需要在单独的线程(并行处理)中执行。然而,问题是代码有时会“挂起”,所以线程需要被“强制”终止。不幸的是,我没有找到任何“精细”的方法来实现:一般建议是告诉线程中的代码优雅地退出,但我无法对这个本地代码(它是第三方代码)这样做。
我使用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没有减少,因此任务执行器仍然“认为”线程正在运行。
  • 我不得不在stopWorkerThread()方法中添加workerThread != null检查,因为这个变量在某些情况下是null。我不明白这些情况是什么...

注:

  • 本地代码不消耗文件描述符(套接字)。所有内容都作为数据块传递给它,并以同样的方式返回。
  • 本地代码需要大量CPU资源。即使它保证会终止,也可能需要很长时间。

赏金编辑:重新审视本地代码的方法/建议已经清晰,请不要在回复中提供此类建议。我需要纯Java解决方案/解决方法。

5个回答

13

Java提供了强制终止线程的纯选项。它古老且已弃用,只有Thread.stop()可用(据我所知)。并且没有安全线程终止的选项(这就是为什么.stop()被弃用的原因,并且允许JVM实现者甚至不实现它)。

原因在于应用程序中的所有线程共享内存和资源--所以,如果在某个任意点上强制终止线程,则无法确定终止的线程是否将一些共享的内存/资源留在不一致的状态中。您甚至不能(一般情况下)假设哪些资源是(可能)脏的(因为您不知道线程停止的确切时间点)。

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

我试图告诉您:您无法使用Java线程/并发执行您要求的事情。我可以建议您的方法(在此处提供)是在单独的进程中完成工作。强制结束进程比线程更安全,因为1)进程之间的分离更加明显,2)操作系统会在进程终止后进行许多清理。杀死进程不是完全安全的,因为存在某些资源(例如文件)默认情况下不会被操作系统清除,但在您的情况下似乎是安全的。

因此,您可以设计一个小型的独立应用程序(甚至可以使用Java--如果第三方库没有提供其他绑定,或者甚至可以使用shell脚本),其唯一的工作是使计算。您从主应用程序启动此类进程,将作业交付给它,并启动看门狗。如果看门狗检测到超时--它将强制终止进程。

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


感谢您的提醒。关于您的帖子,我有一个小建议:由于启动的线程正在执行本地代码,除非您通过JNI明确编写此代码,否则不会存在任何锁与Java部分共享。因此,唯一具有风险的资源是打开的文件描述符(对我来说并非如此,因为所有必要的数据都作为blob传递,并作为字符串返回)。唯一的问题是如何告诉ThreadPoolTaskExecutor线程已经真正死亡?我已经查看了代码:它应该真正捕获这种情况。 - dma_k
1
没有锁 - 可以。但是本地代码中的内存泄漏呢?您能确定本地库被设计和编码得足够安全,以优雅地处理异常终止吗?它是否在所有退出路径上释放了分配的内存,包括异常路径?如果本地代码中有内存泄漏,您会怎么做?同样的问题也适用于某些操作系统级别的资源 - 如果本地库分配了内核级别的锁定?启动了几个额外的线程? - BegemoT
关于堆上的内存泄漏问题,我同意(因为线程共享相同的堆)。虽然我不是专家,但我认为用户线程在内核模式下不能被中断,否则杀死线程可能会损坏文件系统,例如当线程在写入磁盘时被杀死。所以未释放的内核级锁将是非常糟糕的。至于其他方面,我完全支持你的观点:进程更安全。此外,JNI桥是由我从独立进程创建的,所以恢复起来并不费力。 - dma_k
1
我认为最适合的解决方案是实现类似于Apache的方式:主进程应启动多个子进程(例如2),每个子进程应处理X个请求并退出(它将由主进程重新启动)。 - dma_k

2
您可以将这个单一的JNI方法封装到一个单独的Java应用程序中,然后使用java.lang.Process fork出另一个Java进程。然后您可以调用Process.destroy() 在操作系统级别上销毁该进程。
根据您的环境和其他考虑因素,您可能需要一些技巧来找到Java可执行文件,特别是如果您正在构建一些可在不同平台上运行的可重定向软件。另一个问题是IPC,但可以使用Process的输入/输出流来解决。

强制终止一个进程甚至比停止一个线程更不优雅。 - Tudor
4
强制终止进程是安全的(由操作系统处理)--但对于线程不是这样。因此,如果第三方JNI库已知不会产生一些外部资源(例如文件),这些资源在进程终止时不会被操作系统清除,那么强制终止进程是更加优雅的解决方案,也是唯一的解决方案,我想。 - BegemoT

2

你这里使用的绝对是一个丑陋的黑客手段...

首先,线程池线程不应该被单独处理,通常应该让它们一直运行到完成状态,尤其不要使用Thread.stop()来停止线程,即使是普通线程也不推荐这样做。

正如我所说的,使用Thread.stop()从未被建议过,通常会导致线程处于不一致状态,这可能是线程池没有将线程视为"已死"的原因。甚至它可能根本就没有杀掉它。

你有什么关于本地代码挂起的想法吗?我认为你问题的根源在这里,而不是停止线程的部分。线程应该尽可能地一直运行到完成状态。也许您可以找到一个更好的实现,在此基础上正确地运行(或者如果您编写了它,则可以实现不同的东西)。

编辑:至于第三点,由于您正在一个线程中进行赋值并在另一个线程中进行读取,因此您可能需要将当前线程的引用声明为volatile

private volatile Thread workerThread;
编辑2: 我开始明白你的JNI代码只做数字计算,不会打开任何可能留下不一致状态的句柄,如果线程被突然杀死。你能确认这一点吗?
在这种情况下,让我违背自己的建议告诉你,在这种情况下,你可以安全地使用Thread.stop()终止线程。然而,我建议你使用单独的线程而不是线程池线程,以避免使线程池处于不一致的状态(正如你所提到的,它没有看到该线程已经死亡)。这也更实用,因为你不必使用所有这些技巧让线程自行停止,因为你可以直接从主线程调用stop()来停止它,不像线程池线程那样。

Tudor,感谢您指出第三个问题,+1分。是的,我知道为什么本机代码“挂起”:数据处理时间太长,在这种情况下,我希望跳过数据(在我的情况下,为了处理速度丢失一些数据是可以接受的)。您建议检查本机代码,除此之外还有其他建议吗? - dma_k
@dma_k:你有没有保证本地代码最终会终止? - Tudor
@dma_k:所以在超时后忽略线程并让它自行完成并不是一个真正的选项。 - Tudor
我很乐意,但是线程会占用CPU,因为它需要大量计算。 - dma_k
@dma_k:这段代码是否打开了任何文件、套接字或其他可能在你突然终止它时未关闭的东西? - Tudor
显示剩余3条评论

1

由于您正在处理第三方代码,我建议创建一个本地外壳应用程序来处理调用、跟踪和终止这些线程。更好的方法是,如果您的许可协议提供任何形式的支持,请让第三方为您完成。

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


如果我理解正确,您的意思是Java线程放弃处理这种情况? - dma_k

0

我不会重复 Tudor 给出的所有宝贵建议...... 我只是在使用任何队列机制来处理主 Java 应用程序和启动的本地线程之间的通信时,添加一种替代的架构观点...... 这个线程可能是经纪人的客户端,并在发生某些特殊事件(终止)时被通知并采取相应措施(停止长时间运行的作业) 当然,这增加了一些复杂性,但是是一个相当优雅的解决方案...... 当然,如果本地线程不够强大,整体的鲁棒性也不会改变...... 处理本地线程和经纪人之间通信的一种方法是使用类似 STOMP 的接口(许多经纪人 Apache activemq、Oracle 的 MQ 都提供这样的接口)...

希望对你有所帮助 Jerome


这个代理(broker)应该用Java编写吗?如果是,您能为此提供元逻辑吗?按照您的说法,代理应该在另一个单独的线程中运行以处理事件... - dma_k

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接