• CompletionService 与 ExecutorService 获取任务执行结果时的区别


    CompletionService 与 ExecutorService 之间的区别

    在讨论二者之间的区别之前,先交待一下背景。

    看了ElasticSearch Transport模块的源码,里面充满了各种异步回调获取结果,于是就想:为什么不用Callable接口,然后再基于java.util.concurrent.Future#get()获取任务的执行结果呢?

    又因为ES的Transport模块底层是基于Netty实现的,研究了下Netty的获取线程执行结果的方式,,虽然Callable、FutureTask 将提交任务执行"异步化"了,但是在获取任务执行结果的这一步,JDK Future#get() 是阻塞的(超时阻塞),那么,能不能在获取结果的时候也不阻塞呢?有二种渠道实现:

    • 回调机制

      ElasticSearch里面就是大量用到回调机制。由于JDK Future的缺陷,Netty的 ChannelFuture扩展了JDK 的Future接口,并提供了回调机制支持异步获取任务的执行结果。它的源码:io.netty.channel.ChannelFuture的注释非常值得一读。

    • JDK8 里面提供的java.util.concurrent.CompletableFuture

      CompletableFuture 可参考《JAVA8实战》中了解一下

    当然,本文不打算讨论,获取任务执行结果也不阻塞的具体实现方法,而是"先退一步",来分析下:

    • 使用 CompletionService 的 submit 方法 java.util.concurrent.CompletionService#submit(java.util.concurrent.Callable)提交多个任务,如何获取任务的执行结果?
    • 使用 ExecutorService 的submit 方法 java.util.concurrent.ExecutorService#submit(java.util.concurrent.Callable)提交多个任务,如何获取任务的执行结果?

    为什么强调多个任务,因为这里讨论的是多个任务的并发执行。并不是第一个任务执行完成后,才能执行第二个任务。那CompletionService 与 ExecutorService 在获取任务结果的时候的区别是什么?

    先说下结论,如果我们的目标是尽快处理任务的执行结果,而不是必须等到所有的任务都执行完成后,拿到所有的执行结果,才能进行下一步处理,那么使用 CompletionService 是非常有好处的。

    举个例子:一个网站要显示10幅图像,下载完一幅就显示一幅,而不需要将这10幅都下载下来,再统一显示,那就很适合用CompletionService。下载 就是线程要执行的任务,图像 就是任务的执行结果。

    使用ExecutorService时,代码是这样的:

    //保存 Future<Image>,后面遍历 List 获取 Future 结果
    List<Future<Image>> futureList = new ArrayList();
    for(int i = 0; i < 10; i++)
    {
        Future<Image> imageFuture = executorService.submit(downloadTask);//10个下载任务同时并发
        futureList.add(imageFuture);
    }
    
    //获取10个任务的执行结果
    for(int i = 0; i < 10; i++)
    {
      Future<Imapge> future = futureList.get(i);
      Imapge image = future.get();//如果图像尚未下载完成,这里会阻塞
      render(image);//将已经下载好的图像渲染到界面
    }
    

    我们是用List<Future<Image>>保存所有的任务Future,然后在for循环里面遍历List获取结果,假设第一个任务下载第一幅图像,第二个任务下载第2幅图像,以此类推....

    这里的问题是:若第一幅图像未下载完成,但是第2幅、第3幅图像已经下载完了,我们也无法优先获取第2幅、第3幅图像。也就无法将已经先下载下来的图像渲染到界面。

    总结起来讲就是:提交任务,将任务添加到List里面的顺序,与任务实际完成顺序是不相关的。

    而使用 CompletionService,就能解决这个缺陷。它使得我们能够获得那些最先下载好的图像。

    使用 CompletionService时,代码是这样的:

    for(int i = 0; i < 10; i++)
    {
        completionService.submit(downloadTask);//10个下载任务同时并发
    }
    
     for(int i = 0; i<10;i++ )
     {    
        //只要任一幅图像下载下来了,completionService.take()就会返回,从而 get() 到这幅图像
        render(completionService.take().get());
    }
    

    completionService.take()是个阻塞方法,如果10幅图像中都没下载下来,那就阻塞了。但只要有一幅下载下来了,就立即能获得到这幅图像。显然,这里:获取任务的执行结果的顺序与提交任务的顺序无关了。

    这里的实现思路也可参考《Java并发编程实战》中第6章。

    参考:

  • 相关阅读:
    golng切片实现分页
    go mgo包 简单封装 mongodb 数据库驱动
    docker 制作自己的镜像
    mongodb 数据库操作——备份 还原 导出 导入
    Override
    Parallel.For循环与普通的for循环的比较
    C#死锁案例代码
    C#的构造函数在基类和父类中执行顺序
    C#构造函数
    C# 多线程的死锁
  • 原文地址:https://www.cnblogs.com/hapjin/p/10898451.html
Copyright © 2020-2023  润新知