Блокировка на stdin заставляет процесс Java обрабатывать 350 мс для выхода

У меня есть 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 вызовом System.in.read, занимает ~ 120 мс для выхода

Я думал, что как только основной поток достигнет конца, программа закончится, но, очевидно, там будет еще около Thread.sleep отставание (вы можете увидеть это, добавив println после Thread.sleep). Я попробовал t.interrupt t.stop System.exit который должен немедленно прекратить "все", но ни один из них не может заставить программу пропустить 350m задержка дополнительного выхода, похоже, ничего не делая.

Кто-нибудь знает, почему это так, и если что-нибудь, что я могу сделать, чтобы избежать этой задержки?

Ответ 1

Как оказалось, помимо сбора плохих новостей, есть и обходное решение, см. Самую нижнюю часть этой публикации.

Это не полный ответ, просто в голову вошел чек, с сокетами.
Основываясь на этом и эксперименте System.in.read() я воспроизвел тоже, задержка может быть связана с тем, что имеет выдающийся синхронный запрос ввода-вывода в ОС. (Edit: на самом деле это явное ожидание, которое срабатывает, когда потоки не выходят нормально, когда 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(...); line, если 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 srv, int port, Socket s1,s2, быстрее убивать вещи на стороне Java: close() on srv/s1/s2 shuts down 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", один из последних шагов процедуры выключения ожидает 300 миллисекунд, минимум:

// [...] In theory, we
// don't have to wait for user threads to be quiescent, but it 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 on /proc/self/fd/0

Хотя readrecv тоже) завернуто в какую-нибудь магическую вещь RESTARTABLE (https://github.com/openjdk-mirror/jdk7u-hotspot/blob/master/src/os/linux/vm/os_linux.inline.hpp# L168 и read немного ниже - это оболочка для 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 (например, как прервать вызов fread?), Это может не сработать.

На стороне библиотеки readSingle (https://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() вы можете балансировать между тем, чтобы быть невосприимчивым или готовить с процессором. Thread.sleep() корректно проверяет, закрыт ли он, поэтому даже если вы поставите его до 10000, процесс будет быстро завершен.

Ответ 2

Как отмечали другие, прерывание потока не вызывает немедленного прекращения блокирующего ввода-вывода. Ожидание происходит, потому что система в ожидании на входе из файла (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

Если вы хотите избежать ожидания потоков ввода-вывода, вы можете сначала проверить доступность. Затем вы можете выполнить ожидание, используя метод, который прерывается, например 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