mafu
mafu

Reputation: 32640

How can I properly block a thread until timeout starts?

I would like to run several tasks in parallel until a certain amount of time has passed. Let us suppose those threads are CPU-heavy and/or may block indefinitely. After the timeout, the threads should be interrupted immediately, and the main thread should continue execution regardless of unfinished or still running tasks.

I've seen a lot of questions asking this, and the answers were always similar, often along the lines of "create thread pool for tasks, start it, join it on timeout"

The problem is between the "start" and "join" parts. As soon as the pool is allowed to run, it may grab CPU and the timeout will not even start until I get it back.

I have tried Executor.invokeAll, and found that it did not fully meet the requirements. Example:

    long dt = System.nanoTime ();
    ExecutorService pool = Executors.newFixedThreadPool (4);
    List <Callable <String>> list = new ArrayList <> ();
    for (int i = 0; i < 10; i++) {
        list.add (new Callable <String> () {
            @Override
            public String call () throws Exception {
                while (true) {
                }
            }
        });
    }
    System.out.println ("Start at " + (System.nanoTime () - dt) / 1000000 + "ms");
    try {
        pool.invokeAll (list, 3000, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e) {
    }
    System.out.println ("End at " + (System.nanoTime () - dt) / 1000000 + "ms");

Start at 1ms
End at 3028ms

This (27 ms delay) may not seem too bad, but an infinite loop is rather easy to break out of - the actual program experiences ten times more easily. My expectation is that a timeout request is met with very high accuracy even under heavy load (I'm thinking along the lines of a hardware interrupt, which should always work).

This is a major pain in my particular program, as it needs to heed certain timeouts rather accurately (for instance, around 100 ms, if possible better). However, starting the pool often takes as long as 400 ms until I get control back, pushing past the deadline.

I'm a bit confused why this problem is almost never mentioned. Most of the answers I have seen definitely suffer from this. I suppose it may be acceptable usually, but in my case it's not.

Is there a clean and tested way to go ahead with this issue?

Edited to add:

My program is involved with GC, even though not on a large scale. For testing purposes, I rewrote the above example and found that the results given are very inconsistent, but on average noticeably worse than before.

    long dt = System.nanoTime ();
    ExecutorService pool = Executors.newFixedThreadPool (40);
    List <Callable <String>> list = new ArrayList <> ();
    for (int i = 0; i < 10; i++) {
        list.add (new Callable <String> () {
            @Override
            public String call () throws Exception {
                String s = "";
                while (true) {
                    s += Long.toString (System.nanoTime ());
                    if (s.length () > 1000000) {
                        s = "";
                    }
                }
            }
        });
    }
    System.out.println ("Start at " + (System.nanoTime () - dt) / 1000000 + "ms");
    try {
        pool.invokeAll (list, 1000, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e) {
    }
    System.out.println ("End at " + (System.nanoTime () - dt) / 1000000 + "ms");

Start at 1ms
End at 1189ms

Upvotes: 0

Views: 67

Answers (2)

mafu
mafu

Reputation: 32640

Using the VM option -XX:+PrintGCDetails, I found that the GC runs more rarely, but with a far larger time delay than expected. That delay just so happens to coincide with the spikes I experienced.

A mundane and sad explanation for the observed behavior.

Upvotes: 0

VGR
VGR

Reputation: 44292

invokeAll should work just fine. However, it is vital that you write your tasks to properly respond to interrupts. When catching InterruptedException, they should exit immediately. If your code is catching IOException, each such catch-block should be preceded with something like:

} catch (InterruptedIOException e) {
    logger.log(Level.FINE, "Interrupted; exiting", e);
    return;
}

If you are using Channels, you will want to handle ClosedByInterruptException the same way.

If you perform time-consuming operations that don't catch the above exceptions, you need to check Thread.interrupted() periodically. Obviously, checking more often is better, though there will be a point of diminishing returns. (Meaning, checking it after every single statement in your task probably isn't useful.)

if (Thread.interrupted()) {
    logger.fine("Interrupted; exiting");
    return;
}

In your example code, your Callable is not checking the interrupt status at all, so my guess is that it never exits. An interrupt does not actually stop a thread; it just signals the thread that it should terminate itself on its own terms.

Upvotes: 1

Related Questions