线程池任务执行结果管理利器ExecutorCompletionService

630 阅读5分钟

前言

在实际生产中,对线程池肯定不陌生,线程池的作用就是可以高效利用多核CPU的并行能力,将程序从串行执行,转变为并发执行。 其中线程池的一个典型的应用场景:提交一批任务需要并行执行,然后异步等待结果。结果的获取通常实现是利用Future机制,返回每个任务的Future,然后轮询阻塞等待执行结果。但是在一些任务场景下,这种是存在一定问题的

问题和描述

当我并发执行的任务的执行完成时间是不确定的,如果耗时特别久的服务在整个轮询的中间或者靠前为止,将会极大的影响整个流程的执行时间。举一个具体的例子:

  • 当我服务A,要调用5个依赖服务:B,C,D,E,F,他们之间是没有相互依赖关系。
  • 比如某次请求,外部依赖服务耗时(B=100,C=1000,D=10,E=100,F=12)
  • 那整个异步处理结果的耗时至少会变为1000ms,因为其他任务结果的回调处理本来可以在C的等待中间处理完成

对于非在线接口来说,这种耗时是能够接受的。但是对于RT响应特别敏感的服务来说,这样的波动会比较致命的。

所以会存在一种需求:我提交一组任务,按照完成时间的排序将结果返回给我。业务方根据业务需求处理结果。在这种背景下:ExecutorCompletionService就粉墨登场了

ExecutorCompletionService 源码解析

主要功能说明

ExecutorCompletionService 内部维护了一个阻塞双端队列,用来保存线程池中已经完成的结果,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果。

依赖关系

从上面的uml依赖图可以看出,ExecutorCompletionService包含以下依赖:

  • Executor用于执行提交的任务
  • BlockingQueue 阻塞队列,用于保存执行的结果
  • QueueingFuture 内部类,用于包装整个执行的任务

那么就有一个疑问,整个流程到底是如何串联起来的,核心的点就在于QueueingFuture,后面会详细解析

源码解析

构造器

    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }
  • Executor线程池由外部调用方提供,可以省去暴露更多线程池相关参数。完全由使用方自行解决
  • LinkedBlockingQueue双端阻塞队列

任务提交

 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;
    }

将Callable包装为一个RunnableFuture,从上面的uml也可以看出,RunnableFuture也是实现了Runnable接口的,因此肯定是做了一定的封装。来看一下FutureTask的run()函数

任务执行

任务的执行是在线程池中,也就是RunnableFuture中的run函数

public void run() {
        //该状态表示任务已经执行过了
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            //执行真实提交的任务
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    //异常包装
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    //正常结果包装
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                //主要处理中断
                handlePossibleCancellationInterrupt(s);
        }
    }

从上面的代码中可以看到,在run中,对call做了一层封装,增加了执行结果的包装。来看一下set(result)

结果提交

protected void set(V v) {
        //执行结果的变更
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

还是没有看到如何将结果加入到队列,来串联整个过程,那就继续看finishCompletion():


private void finishCompletion() {
        //这里主要通知等待结果的线程,可以获取了
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }
        //这里
        done();
        //便于垃圾回收
        callable = null;        // to reduce footprint
    }

再看一下done():

 protected void done() { }

该放回默认是空,但是QueueingFuture覆盖了该方法

 protected void done() { completionQueue.add(task); }

在这里将结果提交到队列中

结果的获取

主要是take,poll方法

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

public Future<V> poll() {
    return completionQueue.poll();
}

public Future<V> poll(long timeout, TimeUnit unit)
        throws InterruptedException {
    return completionQueue.poll(timeout, unit);
}
    

都在等待结果的返回。依托于阻塞队列

应用场景

需求:

通常在并行执行的任务中,业务调用方希望存在一个最长的等待时间或者成功比例,来确保对主流程造成重大影响。否则依照木桶短板理论,整个服务的稳定性必须依靠耗时最久的任务。在超时和80%结果返回,有些场景还是希望能够获得80%的数据

代码实现

public void test() throws InterruptedException, ExecutionException {
        int coreSize = 10;
        int maxSize = 100;
        int queueSize = 1024;
        ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 60 * 1000L, TimeUnit.SECONDS,
                new LinkedBlockingQueue(queueSize),
                (new ThreadFactoryBuilder()).setNameFormat("test-thread").build(),
                new ThreadPoolExecutor.DiscardPolicy());

        ExecutorCompletionService<Integer> executorService = new ExecutorCompletionService<Integer>(executor);

        List<Callable> taskList = Lists.newArrayList();
        for (int index = 0; index < 9; index++) {
            taskList.add(new Callable() {
                @Override
                public Object call() throws Exception {
                    int num = ThreadLocalRandom.current().nextInt(10, 100);
                    Thread.sleep(num);
                    return num;
                }
            });
        }

        taskList.add(new Callable() {
            @Override
            public Object call() throws Exception {

                int num = 1000;
                Thread.sleep(num);
                return num;
            }
        });
        taskList.stream().forEach(task -> executorService.submit(task));

        // 获取结果,按照最长耗时
        int waitTime = 300;
        int leftTime = waitTime;
        // 等待间隔时间
        int inteval = 10;
        int processCount = 0;

        long statTime = System.currentTimeMillis();
        while (leftTime > 0 && processCount != taskList.size()) {
            long startTime = System.currentTimeMillis();
            Future<Integer> future = executorService.poll(inteval, TimeUnit.MILLISECONDS);
            leftTime -= (System.currentTimeMillis() - startTime);
            if (Objects.nonNull(future)) {
                processCount++;
                System.out.println(future.get());
            }
        }
        System.out.println("cost:" + (System.currentTimeMillis() - statTime));
    }
    

上述的代码就是基于最大容忍度的结果返回:当耗时超过300ms直接返回已有的结果。也可以修改调整为根据一定返回比例+时间的方式区完成 运行结果: 整个结果是跟完成时间直接挂钩,且超过300后,直接丢弃后面的任务

后记

任何计数都有最适配的业务场景,广度和深度都很重要