0

I've a list of Runnable tasks (e.g. it contains 100 tasks, and each task takes randomly 1 - 10 seconds). The tasks must run in parallel and from the thread pool provided by ExecutorService (e.g. my system has 4 CPUs, then it should run 4 tasks at the same time).

The question is: I wanted to know which tasks took longer than 5 seconds to run from the list of 100 tasks and they should be terminated (with logs of task ids) after 5 seconds to have places for other tasks.

I've looked at Future with executorService.submit(Runnable task) but the Future.get() method will block the main thread and it is not what I wanted. Any suggestions would be great.

public class TestExecutorService {

    private static final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);

    public static void main(String[] args) throws InterruptedException {
        List<Callable<Object>> tasks = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            int finalI = i;
            int min = 1;
            int max = 8;

            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        int sleepTime = min + (int)(Math.random() * ((max - min) + 1));
                        System.out.println("## Thread: " + finalI + " will sleep: " + sleepTime + " seconds.");
                        Thread.sleep(sleepTime * 1000);
                        System.out.println("## Thread: " + finalI + " finished after: " + sleepTime + " seconds");
                    } catch (InterruptedException e) {
                        System.out.println("Thread is cancelled!");
                    }
                }
            };

            tasks.add(Executors.callable(runnable));
        }

        // How to make a Runnable task timeout after 5 seconds when running other tasks in parallel
        // instead of total time for 100 tasks in 5 seconds?
        executorService.invokeAll(tasks, 5, TimeUnit.SECONDS);

        executorService.shutdown();
    }
}
Bằng Rikimaru
  • 1,512
  • 2
  • 24
  • 50
  • 1
    Your code does already terminate all jobs after 5 seconds. So what’s your question? – Holger May 23 '23 at 15:16
  • @Holger I wanted to know which Runnable ran more than 5 seconds, I put the code there as an example only. I don't know which method to trigger long running Runnable to interrupt itself. – Bằng Rikimaru May 23 '23 at 16:10
  • 1
    The `invokeAll` does already cancel all tasks running longer than the specified timeout. It’s not clear why you consider this operation “example only” if it does exactly what you described. If you want to know which task has been canceled, use the list of futures returned by the `invokeAll` call and check the `isCancelled()` status. – Holger May 23 '23 at 16:34
  • @Holger `invokeAll` with 5 seconds timeout doesn't do what I wanted. I have a list of 100 `Runnable` tasks and each task will take between 1 - 10 seconds. I have a thread pool with 4 threads (my system has 4 CPUs). So at one time, only 4 tasks can run in parallel. Given 100 tasks will not be able to invoke all of them below 5 seconds before timeout for `invokeAll`. If you don't think so, you could test my code in the description. – Bằng Rikimaru May 23 '23 at 18:17
  • @Holger The question of this ticket is simple to understand, I have 100 `Runnable` tasks and I wanted to run them in parallel in a batch mode with thread pools with maximum 4 threads. I don't care how long all of 100 tasks will take, but after all of them finished (long-running with more than 5 seconds thread names should be logged). – Bằng Rikimaru May 23 '23 at 18:23
  • `invokeAll` returns a List of Futures. Check each Future’s `isDone` method to determine whether that task finished. – VGR May 23 '23 at 23:39
  • Your example creates a fixed pool of `Runtime.getRuntime().availableProcessors() * 10` threads. That’s why it doesn’t demonstrate your issue with four threads. You should further understand that this is not a helpdesk service and you didn’t create a “ticket”. You asked a question to volunteers. If no-one understands the question, no-one will answer. Including the information of your last two comments in the question will raise the chances that someone can answer the question. – Holger May 24 '23 at 06:41

2 Answers2

1

Lets say you have some list of tasks, and an Executor to

List<Runnable> tasks = ...;
ExecutorService executor = ...;

Now you want to perform each task, get the time it takes, and cancel the task if it takes too long. I would suggest scheduling a time out action.

ScheduledExecutorService timeoutService = Executors.newSingleThreadScheduledExecutor();

Now when you're submitting your tasks.

List<Future<Long>> results = new ArrayList<>();
for(int i = 0; i<tasks.size(); i++){
    Runnable task = tasks.get(i);
    Future<Long> future = executor.submit( () ->{
        long start = System.currentTimeMillis();
        task.run();
        return System.currentTimeMillis() - start;
    });
    Future<?> timeout = timeoutService.schedule( ()->{
        if(!future.isDone()){
            future.cancel(true);
        }
    }, 5, TimeUnit.SECONDS);
    results.add(future);
}

Now you can just go through results and call get when all of the tasks have finished, either exceptionally or normally, you will finish going through the results list. This assumes your tasks can be cancelled or interrupted If they cannot, then you can use the timeout futures.

matt
  • 10,892
  • 3
  • 22
  • 34
0

The idea to solve my problem is to use another ExecutorService single-thread which will invoke in each Runnable task. It has its own timeout so it will not interfere with the thread pools with other tasks.

Here is the full code:

package com.company;

import java.sql.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class TestThreads {

    private static final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

    public static void main(String[] args) throws InterruptedException {


        List<Callable<Object>> tasks = new ArrayList<>();
        List<Runnable> runnables = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            int finalI = i;
            int min = 1;
            int max = 20;

            Runnable runnable = new Runnable() {
                @Override
                public void run() {

                    String threadName = "### Thread: " + finalI;

                    long maxTime = 5000;

                    ExecutorService executorServiceTmp = Executors.newSingleThreadExecutor();
                    Callable<Object> callable = () -> {
                        int sleepTime = min + (int) (Math.random() * ((max - min) + 1));
                        System.out.println("## Thread: " + finalI + " will sleep: " + sleepTime + " seconds.");
                        Thread.sleep(sleepTime * 1000);
                        System.out.println("## Thread: " + finalI + " finished after: " + sleepTime + " seconds");
                        return null;
                    };

                    long startTime = System.currentTimeMillis();

                    try {
                        executorServiceTmp.invokeAll(Arrays.asList(callable), maxTime, TimeUnit.MILLISECONDS);
                    } catch (Exception e) {
                        System.out.println("Thread: " + threadName + " is cancelled.");
                    } finally {
                        executorServiceTmp.shutdown();
                    }

                    long endTime = System.currentTimeMillis();
                    long totalTime = endTime - startTime;

                    if (totalTime >= maxTime) {
                        System.out.println("(!) Thread: " + threadName + " is cancelled after " + maxTime + " ms");
                    }

                }
            };

            tasks.add(Executors.callable(runnable));
            runnables.add(runnable);
        }

        executorService.invokeAll(tasks);

        System.out.println("### All threads fininshed");

        executorService.shutdown();

    }
}
Bằng Rikimaru
  • 1,512
  • 2
  • 24
  • 50
  • @matt not the "best solution",but it does what I wanted. Each long-running task will be finished after 5 seconds and it doesn't interfere with other tasks running in parallel. – Bằng Rikimaru May 24 '23 at 07:56
  • 1
    I created a example. To perform the timeout I use *one* additional executor service that schedules the timeout after some allotted time. Also it wraps the runnable tasks and returns the time it took to complete them. – matt May 24 '23 at 08:02
  • 1
    Your code has some really strange aspects to it. You call "invokeAll" with a time out, but then you decide if it has exceed the time to run by calculating the runtime difference. Why not check the result of `invokeAll`? The future will have finished or not. For that matter, don't use invokeAll just submit the task and use the Future.get with a timeout? – matt May 24 '23 at 08:07
  • @matt thanks, your example code and suggestion are more elegant. I changed to not use `invokeAll` and use `Future.get()` with timeout 5 seconds and `Future.cancel(true)` afterwards. – Bằng Rikimaru May 24 '23 at 08:42