Akka工具(二)—Future

       Future被Akka设计用来处理并发执行结果的数据结构,我们可以通过同步(阻塞)或异步(非阻塞)的方法接受结果。使用Future,我们可以在Actor外部获取某个Actor的消息,在介绍Actor消息发送方式时,我们进行过简单介绍,现在我们先来回忆一下Future的常规用法。

    Future消息处理

       ask发送消息会返回一个Future对象,通过该对象我们可以同步或异步方式处理Actor返回的结果消息。同步方式主要依赖Await类的方法阻塞等待返回值,异步方式则依靠Future对象提供的诸多回调方法。使用ask方法时,我们最好设置一个超时时间,否则一直阻塞严重影响系统的吞吐率和性能。

同步方式代码示例:

public class FutureActor extends AbstractActor {

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("system");
        ActorRef futureActor = system.actorOf(Props.create(FutureActor.class), "futureActor");
        Timeout timeout = new Timeout(Duration.create(2, TimeUnit.SECONDS));
        Future future = Patterns.ask(futureActor, "wait access message", timeout);
        try {
            String reply = (String) Await.result(future, timeout.duration());
            System.out.println("回复的消息: " + reply);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder().matchAny(other -> {
            System.out.println("接受的消息:" + other);
            //模拟超时操作
            // Thread.sleep(3000);
            getSender().tell("access", getSelf());
        }).build();
    }
} 
  

       我们首先定义Timeout用于限制超时,然后使用Patterns.ask给futureActor发送异步请求,返回一个Future对象,该对象封装了futureActor返回的消息,之前我们说过处理消息有两种方式:同步和异步,代码中采用Await.result(同步方式),表明我们会在获取消息上阻塞,直到消息返回才会继续往下执行。这里我们使用sleep方法模拟服务超时,程序会抛出TimeoutException。

java.util.concurrent.TimeoutException: Futures timed out after [2 seconds]
    
    at scala.concurrent.impl.Promise$DefaultPromise.ready(Promise.scala:223)

    at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:227)

    at scala.concurrent.Await$$anonfun$result$1.apply(package.scala:190)

    at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:53)

    at scala.concurrent.Await$.result(package.scala:190)

    at scala.concurrent.Await.result(package.scala)

    at com.release.util.akka.Future.FutureActor.main(FutureActor.java:26)

       在实际项目中,如果ask请求结果不影响程序继续执行,为了尽可能提高程序响应速度,我们或许更喜欢异步方式处理消息,Future类提供了诸多回调函数,例如:onComplete(处理执行完成)、onSuccess(处理执行成功)、onFailure(处理执行失败),修改上述示例:

     future.onComplete(new OnComplete() {
            @Override
            public void onComplete(Throwable throwable, Object o) throws Throwable {
                if (throwable != null) {
                    System.out.println("返回结果异常:" + throwable.getMessage());
                } else {
                    System.out.println("返回消息:" + o);
                }
            }
        }, system.dispatcher());
        // 成功,执行过程
        future.onSuccess(new OnSuccess() {
            @Override
            public void onSuccess(Object msg) throws Throwable {
                System.out.println("回复的消息:" + msg);
            }
        }, system.dispatcher());
        //失败,执行过程
        future.onFailure(new OnFailure() {
            @Override
            public void onFailure(Throwable throwable) throws Throwable {
                if (throwable instanceof TimeoutException) {
                    System.out.println("服务超时");
                } else {
                    System.out.println("未知错误");
                }
            }
        }, system.dispatcher());

    } 
  

        当futureActor及时返回消息,onSuccess方法会执行并接受返回的消息;当程序执行超时或其它错误,会调用onFailure方法并得到异常;大家执行可以发现,无论成功与否,onComplete方法都会调用,它相当于onSuccess和onFailure方法整合。

     函数式Future

        相信使用过Jdk1.8或以上的朋友,都知道函数式编程的好处,调用清晰、处理简单,方便我们对各类数据进行分析和调整。在这里Future也提供了很多好用的函数式API,方便我们对数据处理。

     map函数

       当我们需要对返回的数据进行处理,并需要返回新的结果时,可以使用map函数。对数据进行处理后,map函数会返回一个新的Future,不影响之前Future的值,是一个全新的Future。例如:

//对actor返回的消息进行处理,返回新的future
Future map = future.map(new Mapper() {
    @Override
    public String apply(Object msg) {
        return "获得futureActor的结果:" + msg;
    }
}, system.dispatcher());
try {
    String reply = Await.result(map, timeout.duration());
    System.out.println(reply);
} catch (Exception e) {
    e.printStackTrace();
}

        使用map函数,需要new一个Mapper对象,该对象需要两个参数,第一个代表输入类型,第二个代表处理后输出的类型,与apply方法的入参和返回值相对应。

        map函数不仅可以作用于一个Future对象,map函数还可以作用于多个Future对象,当我们获取到一系列Future之后,我们可以把它们组合成一个列表,利用map函数进行计算。例如:统计一个班级上姓李的同学数量。

示例:

public class MapActor extends AbstractActor {
    public static void main(String[] args) {
        String[] names = {"张三", "李四", "王五", "李春花", "李贵妃"};
        ActorSystem system = ActorSystem.create();
        Timeout timeout = new Timeout(Duration.create(3, TimeUnit.SECONDS));
        List> futures = new ArrayList<>();
        for (int i = 0; i < names.length; i++) {
            ActorRef ref = system.actorOf(Props.create(MapActor.class), "mapActor" + i);
            Future future = Patterns.ask(ref, names[i], timeout);
            futures.add(future);
        }
        //把Future列表转换为带有结果列表的Future对象
        Future> iterableFuture = Futures.sequence(futures, system.dispatcher());
        Future map = iterableFuture.map(new Mapper, Integer>() {
            @Override
            public Integer apply(Iterable parameter) {
                Integer count = 0;
                for (Integer o : parameter) {
                    count += o;
                }
                return count;
            }
        }, system.dispatcher());
        try {
            Integer reply = Await.result(map, timeout.duration());
            System.out.println("李姓数量: " + reply);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder().match(String.class, s -> getSender().tell(s.startsWith("李") ? 1 : 0, getSelf())).build();
    }
}

       上述过程中,我们先使用sequence让List集合转换为带有结果列表的Future对象,因为List集合是不能直接使用map方法的,毕竟map方法不属于List,之后使用map方法遍历结果计算,最终返回一个带有我们所需结果的新Future对象。

     fold函数

       我们除了使用map函数统计之外,还可以使用fold函数,fold与map函数有所不同,fold函数提供了一个初始值,当我们传一个带有Future的列表时,fold会先使用初始值和列表的第一个值计算,然后把计算的值作为下一次的初始值,以此类推。修改上述代码:

       Future fold = Futures.fold(0, futures, new Function2() {
            @Override
            public Integer apply(Integer v1, Integer v2) throws Exception {
                System.out.println(v1+"--->"+v2);
                return v1 + v2;
           }
       }, system.dispatcher());

       输出结果:

0--->0 
0--->1 
1--->0 
1--->1 
2--->1
李姓数量: 3

       当然这里完全可以不需要初始值,所以我们也可以使用reduce函数,使用方式和fold类似,如下:

       Future fold = Futures.reduce( futures, new Function2() {
            @Override
            public Integer apply(Integer v1, Integer v2) throws Exception {
                System.out.println(v1+"--->"+v2);
                return v1 + v2;
            }
        }, system.dispatcher());

    总结

       使用Future可以让我们在Actor外部获取结果,并且可以选择获取结果的方式:异步或者同步,尽可能的利用异步方式获取结果,可以提高系统的吞吐率和响应速度。另外,使用Future,我们还可以使用它所提供的诸多函数式API,方便我们对数据进行处理或计算。

你可能感兴趣的:(读Akka实战笔记)