The Thinking Logic of Computer Programs (79) - Convenient Completion Service

Keywords: Java Programming Google github

Upper segment We mentioned that in asynchronous task programs, a common scenario is that the main thread submits multiple asynchronous tasks and then wants to process the results as soon as the task is completed, and process them one by one in the order of task completion. For this scenario, Java concurrent package provides a convenient way to use Completion Service, which is An interface whose implementation class is Executor CompletionService is discussed in this section.

Basic Usage

Interface and class definitions

and 77 quarter Like Executor Service, Completion Service can also submit asynchronous tasks. The difference is that it can obtain results in the order of task completion, which is specifically defined as:

public interface CompletionService<V> {
    Future<V> submit(Callable<V> task);
    Future<V> submit(Runnable task, V result);
    Future<V> take() throws InterruptedException;
    Future<V> poll();
    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}

Its submit method is the same as ExecutorService. It has many take and poll methods. They are both to get the result of the next completed task. take() will block and wait, poll() will return immediately. If there is no completed task, return null, and poll method with time parameter will wait for the limited time at most.

The main implementation class of Completion Service is Executor Completion Service, which relies on an Executor to complete the actual task submission, while it is mainly responsible for queuing and processing the results. Its construction method has two ways:

public ExecutorCompletionService(Executor executor)
public ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)

At least one Executor parameter is needed to provide a BlockingQueue parameter for the queue that completes the task. If not, a Linked BlockingQueue is created inside the Executor Completion Service.

Basic examples

We are 77 quarter The invokeAll example demonstrates concurrent downloading and analysis of the title of the URL. In that case, the result is not processed until all tasks are completed. Here, we modify it and output the result as soon as the task is completed. The code is as follows:

public class CompletionServiceDemo {
    static class UrlTitleParser implements Callable<String> {
        private String url;

        public UrlTitleParser(String url) {
            this.url = url;
        }

        @Override
        public String call() throws Exception {
            Document doc = Jsoup.connect(url).get();
            Elements elements = doc.select("head title");
            if (elements.size() > 0) {
                return url + ": " + elements.get(0).text();
            }
            return null;
        }
    }

    public static void parse(List<String> urls) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        try {
            CompletionService<String> completionService = new ExecutorCompletionService<>(
                    executor);
            for (String url : urls) {
                completionService.submit(new UrlTitleParser(url));
            }
            for (int i = 0; i < urls.size(); i++) {
                Future<String> result = completionService.take();
                try {
                    System.out.println(result.get());
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            executor.shutdown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        List<String> urls = Arrays.asList(new String[] {
                "http://www.cnblogs.com/swiftma/p/5396551.html",
                "http://www.cnblogs.com/swiftma/p/5399315.html",
                "http://www.cnblogs.com/swiftma/p/5405417.html",
                "http://www.cnblogs.com/swiftma/p/5409424.html" });
        parse(urls);
    }
}

In the parse method, an ExecutorService is created first, and then a Completion Service, through which tasks are submitted and the results are processed one by one in the order of completion. Is this convenient?

Basic Principles

How does Executor Completion Service make the results processed in an orderly manner? In fact, it's also very simple. As mentioned earlier, it has an additional queue, and after each task is completed, Future s representing the results will be included in the queue.

The question is, after the task is completed, how do you know to join the team? Let's look at it in detail.

stay 77 quarter We have introduced FutureTask. When a task is completed, the finishCompletion method is invoked whether it completes normally, ends abnormally or is cancelled. This method calls a done method. The code of this method is as follows:

protected void done() { }

Its implementation is empty, but it is a protected method, which can be overridden by subclasses.

In ExecutorCompletion Service, the task type submitted is not a general FutureTask, but a subclass QueueingFuture, as follows:

public Future<V> submit(Callable<V> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<V> f = newTaskFor(task);
    executor.execute(new QueueingFuture(f));
    return f;
}

This subclass overrides the done method and adds the results to the completion queue when the task is completed. The code is as follows:

private class QueueingFuture extends FutureTask<Void> {
    QueueingFuture(RunnableFuture<V> task) {
        super(task, null);
        this.task = task;
    }
    protected void done() { completionQueue.add(task); }
    private final Future<V> task;
}

The take/poll method of ExecutorCompletionService retrieves the results from the queue, as follows:

public Future<V> take() throws InterruptedException {
    return completionQueue.take();
}

Implementing invokeAny

We are 77 quarter Referring to the implementation of invokeAny of AbstractExecutor Service, we use Executor Completion Service. Its basic idea is that after submitting a task, we can get the result through take method, and then cancel all other tasks after obtaining the first effective result. However, its specific implementation has some optimization and complexity. Let's look at an example of a simulation that queries a keyword from multiple search engines, but only one result can be obtained. The simulation code is as follows:

public class InvokeAnyDemo {
    static class SearchTask implements Callable<String> {
        private String engine;
        private String keyword;

        public SearchTask(String engine, String keyword) {
            this.engine = engine;
            this.keyword = keyword;
        }

        @Override
        public String call() throws Exception {
            // Simulate search results from a given engine
            Thread.sleep(engine.hashCode() % 1000);
            return "<result for> " + keyword;
        }
    }

    public static String search(List<String> engines, String keyword)
            throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        CompletionService<String> cs = new ExecutorCompletionService<>(executor);
        List<Future<String>> futures = new ArrayList<Future<String>>(
                engines.size());
        String result = null;
        try {
            for (String engine : engines) {
                futures.add(cs.submit(new SearchTask(engine, keyword)));
            }
            for (int i = 0; i < engines.size(); i++) {
                try {
                    result = cs.take().get();
                    if (result != null) {
                        break;
                    }
                } catch (ExecutionException ignore) {
                    // Exceptions, invalid results, continue
                }
            }
        } finally {
            // Cancellation of all tasks is ineffective for completed tasks
            for (Future<String> f : futures)
                f.cancel(true);
            executor.shutdown();
        }
        return result;
    }

    public static void main(String[] args) throws InterruptedException {
        List<String> engines = Arrays.asList(new String[] { "www.baidu.com",
                "www.sogou.com", "www.so.com", "www.google.com" });
        System.out.println(search(engines, "Lao Ma said programming"));
    }
}

SearchTask simulates querying results from a specified search engine. Search uses Completion Service/ExecutorService to execute concurrent queries. After obtaining the first valid result, it cancels other tasks.

Summary

This section is relatively simple, mainly introduces the use and principle of Completion Service, which facilitates the processing of the results of multiple asynchronous tasks through an additional result queue.

In the next section, we discuss a common requirement, timing tasks.

(As in other chapters, all the code in this section is located at https://github.com/swiftma/program-logic)

----------------

To be continued, check the latest articles, please pay attention to the Wechat public number "Lao Ma Says Programming" (scanning the two-dimensional code below), from the entry to advanced, in-depth shallow, Lao Ma and you explore the essence of Java programming and computer technology. Be original and reserve all copyright.

Posted by Shroder01 on Tue, 09 Jul 2019 12:26:24 -0700