• 同时处理多请求(带超时时间)


    在上一篇 同时处理多个请求,记录了同时处理多个请求的几种方式,本篇主要介绍多线程处理时,进行超时控制。也就是说超时了的任务扔掉,未超时的任务返回

    在研究线程相关的API时,发现了future.get(timeout, unit)方法,意思是在指定的时间内会等待任务执行,超时则抛异常。激动之余赶紧试了下:

    修改下ParallelService中的接口(三个请求的用时分别是1s、2s、10s):

    @Slf4j
    @Service
    public class ParallelService {
    
        public String requestA() {
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                log.info("requestA被打断");
            }
            return "A";
        }
    
        public String requestB() {
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                log.info("requestB被打断");
            }
            return "B";
        }
    
        public String requestC() {
            try {
                TimeUnit.MILLISECONDS.sleep(10000);
            } catch (InterruptedException e) {
                log.info("requestC被打断");
            }
            return "C";
        }
    
    }

    增加测试方法(超时时间是3s,也就是说超过3s的任务会被扔掉):

       /**
         * 多线程请求(带超时时间)
         */
        @GetMapping("/test4")
        public void test4() {
            long start = System.currentTimeMillis();
    
            List<String> list = new ArrayList<>();
            List<Future<String>> futureList = new ArrayList<>();
            ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
            IntStream.range(0, 3).forEach(index -> {
                Future<String> task = executor.submit(() -> request(index));
                futureList.add(task);
            });
            for (int i = 0; i < futureList.size(); i++) {
                Future<String> future = futureList.get(i);
                try {
                    // future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。
                    String result = future.get(3, TimeUnit.SECONDS);
                    log.info("结果:{}", result);
                    list.add(result);
                } catch (TimeoutException e) {
                    log.info("task{},超时", i);
                    // 强制取消该任务
                    future.cancel(true);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
            // 停止接收新任务,原来的任务继续执行
            executor.shutdown();
    
            log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
        }

    发送请求,结果:

    表面上看感觉效果达到了,时长超过3s的请求被扔掉了。但仔细看发现了个问题:响应时长为啥是5s左右,不应该是3s吗?

    在future.get方法前后加上日志:

    long start1 = System.currentTimeMillis();
    // future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。
    String result = future.get(3, TimeUnit.SECONDS);
    log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1);
    list.add(result);

    重新请求一次,结果:

    好奇怪,为啥requestB的用时是1s呢?通过多次试验终于发现了真相:

    future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。任务执行的时间是获取到结果的时长。由于每个任务是同时执行的, 但是获取结果时,是阻塞的,也就是串行获取的,所以每个任务获取结果的时长 = 当前任务请求时长 - 上一个任务请求时长。

    由此可以计算出任务a时长是1s,任务b是2-1=1s,任务c是10-2=8s。至于总时长5s = 任务b获取结果用时2s + 超时时间3s

    结论:当只有一个任务时,超时时间有效,当多个任务执行时,超时时间无效

    那如果将future.get(timeout, unit) 方法放在一个子线程中,异步去获取结果,能达到效果吗?拭目以待:

       /**
         * 多线程请求(带超时时间)
         */
        @GetMapping("/test5")
        public void test5() {
            long start = System.currentTimeMillis();
    
            List<String> list = new ArrayList<>();
            List<Future<String>> futureList = new ArrayList<>();
            ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
            IntStream.range(0, 3).forEach(index -> {
                Future<String> task = executor.submit(() -> request(index));
                futureList.add(task);
            });
            for (int i = 0; i < futureList.size(); i++) {
                Future<String> future = futureList.get(i);
                ExecutorService executor2 = Executors.newSingleThreadExecutor();
                int j = i;
                executor2.execute(() -> {
                    try {
                        // 在指定的时间内会等待任务执行,超时则抛异常
                        long start1 = System.currentTimeMillis();
                        String result = future.get(2, TimeUnit.SECONDS);
                        log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1);
                        list.add(result);
                    } catch (TimeoutException e) {
                        log.info("task{},超时", j);
                        future.cancel(true);
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                    }
                });
            }
    
            // 停止接收新任务,原来的任务继续执行
            executor.shutdown();
    
            while (true) {
                // 将future.get放入子线程后,由于不会阻塞,所以就直接运行到下面。需要通过判断所有线程是否结束来获取最终结果
                if (executor.isTerminated()) {
                    log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
                    break;
                }
            }
        }

    请求一次,结果:

     完美达到我们的效果!再来压测一下:

    从JMeter结果可以看到:平均响应时长:3438ms,最小响应时长:2415ms,最大响应时长:4707ms,TPS:8.1/sec

    结论:虽然代码复杂一点,但是效果基本达到了,不过有一点,开启的线程的翻了一倍,对内存消耗比较大

    再次研究api,找到了最终的大招,使用invokeAll(tasks, timeout, unit)方法:

       /**
         * 多线程请求(带超时时间)
         */
        @GetMapping("/test6")
        public void test6() {
            long start = System.currentTimeMillis();
    
            List<String> list = new ArrayList<>();
            ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
            List<Callable<String>> callableList = new ArrayList<>();
            IntStream.range(0, 3).forEach(index -> {
                callableList.add(() -> request(index));
            });
            try {
                log.info("开始执行");
                long start1 = System.currentTimeMillis();
                // invokeAll会阻塞。必须等待所有的任务执行完成后统一返回,这里的超时时间是针对的所有tasks,而不是单个task的超时时间。
                // 如果超时,会取消没有执行完的所有任务,并抛出超时异常
                List<Future<String>> futureList = executor.invokeAll(callableList, 2, TimeUnit.SECONDS);
                log.info("执行完,用时:{}", System.currentTimeMillis() - start1);
                for (int i = 0; i < futureList.size(); i++) {
                    Future<String> future = futureList.get(i);
                    try {
                        list.add(future.get());
                    } catch (CancellationException e) {
                        log.info("超时任务:{}", i);
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                    }
                }
            } catch (InterruptedException e1) {
                log.info("线程被中断");
            }
    
            // 停止接收新任务,原来的任务继续执行
            executor.shutdown();
    
            log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
        }

    请求一次,结果:

    完美,太完美了,再来压测一下:

    从JMeter结果可以看到:平均响应时长:2114ms,最小响应时长:2007ms,最大响应时长:2485ms,TPS:13.3/sec

    结论:代码很简洁,效率也很好

  • 相关阅读:
    LTE学习之路(3)——概述(续)
    LTE学习之路(2)——概述
    LTE学习之路(1)——移动通信技术发展历程
    读书笔记:C++ Primer系列(14)—— C++函数及参数传递
    读书笔记:C++ Primer系列(13)—— break、continue、goto语句
    读书笔记:C++ Primer系列(12)—— 类型转换
    qrcodejs二维码合成海报
    vue-core-image-upload 是一款轻量级的 Vue.js 上传插件,它可以支持的图片的上传,裁剪,压缩。它同样也支持在移动端的图片处理,它定义了诸多上传周期,你可以自由的进行流程控制。
    vue-cli按需加载,懒加载组件
    vue-cli 部分浏览器不支持es6的语法-babel-polyfill的引用和使用
  • 原文地址:https://www.cnblogs.com/xuwenjin/p/11314765.html
Copyright © 2020-2023  润新知