异步回调编程

889 阅读8分钟

什么是异步回调

是指在异步任务执行完后进行回调处理,且不会阻塞当前线程

备注:异步任务可以有返回值也可以无返回值,若有返回值则回调处理时可以拿到异步任务的返回值

异步回调编程的作用是什么?为什么要用?

假设一个场景:

主线程开启了一个异步任务,这个异步任务执行完后会返回一个结果,主线程想拿这个结果进一步做些其他事情。

若没有异步回调能力:

主线程需要等待异步任务执行完,等待期间不能继续推进“主线任务”效率低

若有异步回调能力:

主线程开启异步任务后注册一个回调处理即可,主线程可以继续推进“主线任务” ,而异步任务在执行完后会自动执行回调处理,效率高

自己思考下如何实现异步回调

首先,思考如何在异步任务执行完后进行处理呢?

最简单的方案:当前线程开启异步任务后等待异步任务执行完毕,然后进行处理。

缺陷:当前线程会阻塞,直到异步任务执行完毕。(这不叫异步回调,而叫同步回调,因为当前线程要等异步任务执行完)

优化一下:不让当前线程等待,那就让其他线程等待并进行后续处理,这不就实现了异步回调嘛。


其实,想要实现异步回调很简单,不就是在异步任务执行完后执行后置处理嘛,方案有:

  1. 方案一:其他线程等待异步任务执行完,然后再执行回调处理。

缺陷:会阻塞线程,浪费线程资源。

  1. 方案二:异步任务线程自己在执行完异步任务后,再触发回调处理。

也就是说,异步任务线程除了要执行异步任务,还要进行回调处理

优点:由异步任务线程自己在执行完异步任务后主动触发回调处理,不会阻塞线程、不会浪费线程资源

对方案二的一个简单畅想:

扩展JDK的FutureTask,重写run方法:

 public void run() {
     try {
         super.run();//执行异步任务
         // onSucess回调处理
     } catch(Exception e) {
         // onFailure回调处理
     }
 }

自己实现一个简陋的异步回调

@Slf4j
public class SimpleDemo {

    /**
     * 第一种实现方式:异步任务线程执行完异步任务后,自己触发回调处理
     */
    @Test
    public void Test1() throws InterruptedException {
        // 定义异步任务
        Callable<String> task = () -> {
            for (int i = 0; i < 3; i++) {
                log.info("task1 run[{}]...", i);
                TimeUnit.MILLISECONDS.sleep(500);
            }
            return "task1 success";
        };
        FutureTask<String> futureTask = new FutureTask<>(task);
        // 执行异步任务
        new Thread(() -> {
            futureTask.run();

            // 在异步任务执行完后,异步线程自己触发回调处理
            try {
                String outcome = futureTask.get();
                log.info("回调处理...任务执行结果:{}", outcome);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).start();

        log.info("主线程开启异步任务后继续推进主线任务...");

        while (true) {
            TimeUnit.SECONDS.sleep(1);
        }
    }

    /**
     * 第二种实现方式:其他线程等待异步任务完成再进行回调处理
     * @throws InterruptedException
     */
    @Test
    public void Test2() throws InterruptedException {
        // 定义异步任务
        Callable<String> task = () -> {
            for (int i = 0; i < 3; i++) {
                log.info("task2 run[{}]...", i);
                TimeUnit.MILLISECONDS.sleep(500);
            }
            return "task2 success";
        };
        FutureTask<String> futureTask = new FutureTask<>(task);
        // 执行异步任务
        new Thread(futureTask).start();
        // 开启另一个线程对异步任务进行回调处理,不阻塞主线程
        new Thread(() -> {
            String result = null;
            try {
                // 等待异步任务执行完
                result = futureTask.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            log.info("回调处理...任务执行结果:{}", result);
        }).start();

        log.info("主线程开启异步任务后继续推进主线任务...");

        while (true) {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

总结一下实现异步回调的思路

两个思路:

  1. 其他线程等待异步任务执行完,然后触发回调处理。(Future接口提供了方法使其他线程可以判断异步任务是否执行完成)
  2. 异步任务线程自己在异步任务执行完后,触发回调处理。

两个方式都不会阻塞主线程(或当前线程)。

学习第三方异步回调框架

学习别人如何实现异步回调的(有什么优点?),寻找自己的不足,为什么自己想不到?有什么收获?

Guava的异步回调

直接看demo学习:

@Slf4j
public class TestGuava {

    @Test
    public void guavaTest() throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        // 装饰者模式,增强线程池,submit方法会返回ListenableFuture,实际还是委托threadPool来执行任务的
        ListeningExecutorService decoratedPool = MoreExecutors.listeningDecorator(threadPool);
        boolean throwException = false;
        ListenableFuture<?> listenableFuture = decoratedPool.submit(() -> {
            log.info("===task start===");
            try {
                log.info("===task run===");
                Thread.sleep(5000);
//                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error("", e);
            }
            // 用于测试异常的
            if (throwException) {
                log.error("===task异常===");
                throw new RuntimeException("测试异常");
            }
            log.info("===task end===");
            return "hello world";
        });

//        log.info("sleep 3s");
//        Thread.sleep(3000);

        // 注册异步回调处理
        Futures.addCallback(listenableFuture, new FutureCallback<Object>() {
            @Override
            public void onSuccess(Object result) {
                log.info("onSuccess, result:{}", result);
            }

            @Override
            public void onFailure(Throwable t) {
                log.info("===onFailure===");
            }
        }, decoratedPool);

        log.info("主线程开启异步任务后继续推进主线任务...");
        while (true) {
            Thread.sleep(3000);
        }
    }

}

大致讲解:

  1. ListenableFuture:表示异步任务

  2. FutureCallback:表示异步回调

  3. Guava线程池:ListeningExecutorService decoratedPool = MoreExecutors.listeningDecorator(threadPool);装饰了JDK的线程池,进行了增强。

实现原理:

有兴趣可以debug源码,很简单

注册回调时,会检查异步任务是否执行完:

  1. 若异步任务已经执行完了,则直接让线程池执行回调任务——CallbackListener(封装了ListenableFutureFutureCallback);
  2. 若异步任务还没有执行完:如果还是直接让线程池执行回调任务,则会阻塞线程,浪费线程资源;因此guava的做法是,此时只将回调任务加入到「回调任务列表」中,然后异步任务线程执行完异步任务后会「主动触发」回调处理:将所有回调任务给线程池执行
看源码:
CallbackListener就是回调处理任务,是Runnable类型,封装了Future和FutureCallback,它的run方法实现了执行回调的逻辑;
异步线程执行完异步任务会调用AbstractFuture#complete(com.google.common.util.concurrent.AbstractFuture)这个方法主动触发回调处理

Netty的异步回调

直接看demo学习:

@Slf4j
public class TestNetty {
    @Test
    public void NettyTest() throws InterruptedException {
        EventExecutor executor = new SingleThreadEventExecutor(null, new DefaultThreadFactory(SingleThreadEventExecutor.class), true) {
            @Override
            protected void run() {
                Runnable task = takeTask();
                if (task != null) {
                    task.run();
                }
            }
        };
        final Promise<String> promise = new DefaultPromise<String>(executor);

        // 立即完成异步任务,测试如何触发监听器。结果:注册监听器时,发现异步任务已经完成了,主线程会立即触发通知监听器:会开启另一个线程通知监听器,有其他线程执行回调逻辑。
//        promise.setSuccess("immediately");

        promise.addListener(new GenericFutureListener<Future<? super String>>() {
            @Override
            public void operationComplete(io.netty.util.concurrent.Future<? super String> future) throws Exception {
                log.info("任务执行完成,result:{}", future.get());
            }
        });

//        promise.sync(); // 测试

        // 先注册监听器,过一会儿才完成任务,测试如何触发监听器。结果:注册监听器时,异步任务未完成,只会保存监听器列表中;在异步任务完成后(setSuccess),(异步线程)会主动触发通知监听器(此处,因为是主线程setSuccess,所以是主线程触发):也是开启另一个线程,由其他线程执行回调逻辑。
        Thread.sleep(3000);
        promise.setSuccess("delay");

        log.info("主线程开启异步任务后继续推进主线任务...");
        while (true) {
            Thread.sleep(100000);
        }
    }
}

大致讲解:

Future(和JDK的Future接口同名)表示异步任务, GenericFutureListener用于监听异步任务,当任务完成后执行回调处理。 使用了观察者模式:「主题」是异步任务Future 「观察者」是监听器GenericFutureListener,当Future完成时,会通知Listener,触发回调处理。

实现原理:

注册监听器时,会检查异步任务是否执行完:

  1. 若已经执行完,则主线程会直接触发通知监听器:实际是开启另一个线程通知监听器,由其他线程执行回调逻辑;

  2. 若没有执行完,则只会将Listener保存到列表中;然后,等到异步任务执行完后(setSuccess方法,一般是异步线程自己调用setSuccess),由异步任务线程「主动触发」通知监听器:也是开启另一个线程,由其他线程执行回调逻辑。

总结

Guava和Netty的异步回调体系都是扩展了JDK Future体系,进行了增强


我思考的方案(即扩展FutureTask)的缺点

  1. 只能注册一个回调

  2. 必须在构造异步任务时,就设置好回调,再提交给线程池执行。


第三方异步回调框架的优点

  1. 一个异步任务,能注册多个回调处理(保存到一个回调处理列表中)。

  2. 可以随时给异步任务注册回调,注册回调时都会检查异步任务是否执行完成

    1. 若异步任务已经完成,则由当前线程(注册回调的线程)直接触发回调处理。
    2. 若异步任务没有完成,则只保存回调到回调列表中;之后,等异步线程执行完异步任务后,由异步任务线程自己触发回调处理。
  3. 都会有一个专门的线程池执行回调处理逻辑。

可以看到,第三方异步回调框架实现异步回调的思路就是让异步任务线程在执行完异步任务后「主动」触发回调处理。 当然,还有个很关键的点是:由于能随时注册回调,所以在注册回调时会检查异步任务是否已经完成,若已经完成则直接触发回调处理。

总结异步回调框架的能力

  1. 创建异步任务

  2. 执行异步任务

  3. 获取异步任务执行结果

  4. 给异步任务添加回调处理,且支持随时添加、也支持添加多个

抽象设计:

  1. 对于1、2和3,需要有一个【异步任务】抽象
  2. 对于4,需要有个【回调处理】抽象

感兴趣的可以自己实现一个异步回调框架。