在stdin上阻塞使得Java进程需要350ms才能退出

问题描述 投票:23回答:2

我有一个Java程序如下:

public class foo{

  public static void main(String[] args) throws Exception{
    Thread t = new Thread(
      new Runnable() {
        public void run() {
          try{System.in.read();}catch(Exception e){}
        }
      }
    );
    t.setDaemon(true);
    t.start();
    Thread.sleep(10); // Make sure it hits the read() call
    t.interrupt();
    t.stop();
    System.exit(0);
  }
}

运行此(time java foo)与System.in.read调用存在需要约480毫秒退出运行它与System.in.read调用注释需要约120毫秒退出

我曾经想过,一旦主线程到达终点,程序就会终止,但很明显,还有另外300ms的滞后(你可以通过在Thread.sleep之后添加一个println来看到这一点)。我试过t.interrupt t.stop System.exit应该“立即”停止事情,但是他们似乎都没有能够让程序跳过它350ms的额外退出延迟似乎什么都不做。

任何人都知道为什么会这样,如果有什么我可以做的,以避免这种延迟?

java
2个回答
18
投票

事实证明,除了收集坏消息之外,还有一些解决方法,请看这篇文章的最后部分。

这不是一个完整的回复,只是 一个支票突然出现在我的脑海里,带有插座。 基于此和我已经复制的System.in.read()实验,延迟可能是对操作系统具有出色的同步I / O请求的成本。 (编辑:实际上它是一个明确的等待,当线程在VM关闭时没有正常退出时启动,见水平线下方)

(我用while(true);结束线程,所以它(它们)永远不会过早退出)

  • 如果你创建一个绑定套接字final ServerSocket srv=new ServerSocket(0);,退出仍然是'正常'
  • 如果你srv.accept();,你突然有额外的等待
  • 如果你用Socket s=new Socket("localhost",srv.getLocalPort());Socket s=srv.accept();创建一个“内部”守护程序线程,那么它再次变为“正常”
  • 但是如果你在其中任何一个上调用s.getInputStream().read();,你就会有额外的等待
  • 如果你用两个插座做,额外的等待延长了一点(远低于300,但对我来说一致20-50毫秒)
  • 如果没有在外面调用new Socket(...);,那么有内螺纹,也有可能卡在accept()线上。这也有额外的等待

所以有套接字(刚绑定甚至连接)不是问题,但等待发生的事情(accept()read())引入了一些东西。

代码(此变体击中两个s.getInputSteam().read()-s)

import java.net.*;
public class foo{
  public static void main(String[] args) throws Exception{
    Thread t = new Thread(
      new Runnable() {
        public void run() {
          try{
            final ServerSocket srv=new ServerSocket(0);

            Thread t=new Thread(new Runnable(){
              public void run(){
                try{
                  Socket s=new Socket("localhost",srv.getLocalPort());
                  s.getInputStream().read();

                  while(true);
                }catch(Exception ex){}
            }});
            t.setDaemon(true);
            t.start();

            Socket s=srv.accept();

            s.getInputStream().read();

            while(true);
          }catch(Exception ex){}
        }
      }
    );
    t.setDaemon(true);
    t.start();
    Thread.sleep(1000);
  }
}

我也尝试了评论中出现的内容:访问(我只是使用static)到ServerSocket srvint portSocket s1,s2,在Java方面杀死东西的速度更快:close() / srv / s1上的s2关闭accept()read()呼叫非常快,特别是关闭accept()new Socket("localhost",port)也可以工作(当实际连接同时到达时,它就有竞争条件)。使用close()也可以关闭连接尝试,只需要一个对象(因此必须使用s1=new Socket();s1.connect(new InetSocketAddress("localhost",srv.getLocalPort()));而不是连接构造函数)。

TL; DR:对你来说重要吗?完全没有:我尝试了System.in.close();,它对System.in.read();绝对没有影响。


新的坏消息。当一个线程使用本机代码,并且该本机代码不检查'safepoint'时,关闭过程waits for 300 milliseconds的最后一步,最小值:

// [...] In theory, we
// don't have to wait for user threads to be quiescent, but it's always
// better to terminate VM when current thread is the only active thread, so
// wait for user threads too. Numbers are in 10 milliseconds.
int max_wait_user_thread = 30;                  // at least 300 milliseconds

而且它等待是徒劳的,因为线程在fread上执行一个简单的/proc/self/fd/0

虽然read(和recv也是)被包裹在一些神奇的RESTARTABLE循环的东西(https://github.com/openjdk-mirror/jdk7u-hotspot/blob/master/src/os/linux/vm/os_linux.inline.hpp#L168read有点低 - 它是另一个文件中fread的包装),这似乎是知道EINTR

#define RESTARTABLE(_cmd, _result) do { \
    _result = _cmd; \
  } while(((int)_result == OS_ERR) && (errno == EINTR))

[...]

inline size_t os::restartable_read(int fd, void *buf, unsigned int nBytes) {
  size_t res;
  RESTARTABLE( (size_t) ::read(fd, buf, (size_t) nBytes), res);
  return res;
}

,但这不会发生在任何地方,加上有一些评论,他们不想干扰libpthread自己的信号和处理程序。根据SO上的一些问题(如How to interrupt a fread call?),它可能无论如何都行不通。

在库方面,readSinglehttps://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/native/java/io/io_util.c#L38)是被调用的方法:

jint
readSingle(JNIEnv *env, jobject this, jfieldID fid) {
    jint nread;
    char ret;
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        return -1;
    }
    nread = (jint)IO_Read(fd, &ret, 1);
    if (nread == 0) { /* EOF */
        return -1;
    } else if (nread == JVM_IO_ERR) { /* error */
        JNU_ThrowIOExceptionWithLastError(env, "Read error");
    } else if (nread == JVM_IO_INTR) {
        JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL);
    }
    return ret & 0xFF;
}

它能够在JRE级别上处理'被打断',但是这部分不会被执行,因为fread没有返回(在所有非Windows的情况下,IO_Read#define-d到JVM_Read,这只是一个wrapper对于前面提到的restartable_read

所以,它是设计的。

有一件事是提供你自己的System.in(尽管final有一个setIn()方法用于此目的,在JNI进行非标准交换)。但它涉及民意调查,所以有点难看:

import java.io.*;
public class foo{
  public static void main(String[] args) throws Exception{

    System.setIn(new InputStream() {
      InputStream in=System.in;
      @Override
      public int read() throws IOException {
        while(in.available()==0)try{Thread.sleep(100);}catch(Exception ex){}
        return in.read();
      }
    });

    Thread t = new Thread(
      new Runnable() {
        public void run() {
          try{
            System.out.println(System.in.read());
            while(true);
          }catch(Exception ex){}
        }
      }
    );
    t.setDaemon(true);
    t.start();
    Thread.sleep(1000);
  }
}

使用Thread.sleep()中的InputStream.read(),您可以在无响应或使用CPU烹饪之间取得平衡。 Thread.sleep()正确检查是否被关闭,所以即使你把它放到10000,这个过程也会快速退出。


8
投票

正如其他人所评论的那样,线程中断不会导致阻塞I / O调用立即停止。等待正在发生,因为系统正在等待来自File(stdin)的输入。如果您要为程序提供一些初始输入,您会发现它完成得更快:

$ time java -cp build/libs/testjava.jar Foo

real    0m0.395s
user    0m0.040s
sys     0m0.008s
$ time java -cp build/libs/testjava.jar Foo <<< test

real    0m0.064s
user    0m0.036s
sys     0m0.012s

如果要避免等待I / O线程,则可以先检查可用性。然后,您可以使用可中断的方法(如Thread.sleep)执行等待:

while (true) {
    try {
        if (System.in.available() > 0) {
            System.in.read();
        }
        Thread.sleep(400);
    }
    catch(Exception e) {

    }
}

该程序每次都会快速退出:

$ time java -cp build/libs/testjava.jar Foo

real    0m0.065s
user    0m0.040s
sys     0m0.008s
$ time java -cp build/libs/testjava.jar Foo <<< test

real    0m0.065s
user    0m0.040s
sys     0m0.008s
© www.soinside.com 2019 - 2024. All rights reserved.