异步编程二:promise模式

书接上回,我们聊了为什么要有异步编程模式以及异步编程的魅力。
本文主要聊一聊异步编程中的promise模式;promise模式广泛应用于前端开发
,所以故事先从前端开发讲起。
因为前端的ajax技术,与服务端通信皆是基于异步编程,需要处理各种回调;
回调本身并不可怕,但是如果需要对多个回调结果进行compose, 那就比较麻烦了,会出现所谓的回调地狱;在异步编程方面,前端同学先痛起来了,所以,在这个方向上,前端同学确实走的也比较靠前,模式也比较成熟。
那么对于java而言,先不谈协程的玩法,异步编程可选的模式就不多了,其中一个便是promise模式,以及接下来会进行介绍的reactor模式。

接下来将按照案例对promise模式进行安利。
(特别提示,本系列文章皆是采用kotlin编写,写过kotlin之后,真的很难切回java,由俭入奢易,由奢入俭难啊)

案例一

有一远程服务名曰“计算器”,提供加法和减法两个http接口

  • 计算 n + m :/add?a=n&b=m
  • 计算 n - m :/sub?a=n&b=m
    基于上面的计算器服务,需要在我们的服务中开发一个接口,计算 m + n - l, 采用异步代码该如何写呢?此处基于vert.x进行示范
 override fun start(startPromise: Promise<Void>?) {
    webClient = WebClient.create(vertx)

    var eventBus = vertx.eventBus()
    eventBus.consumer<JsonObject>("calc.promise"){ msg ->
        var msgBody = msg.body()
        var m = msgBody.getInteger("m", 0)
        var n = msgBody.getInteger("n", 0)
        var l = msgBody.getInteger("l", 0)

        webClient.get(7777, "pi", "/add?a=$m&b=$n").send{
            if (it.succeeded()) {
                var `m + n` = it.result().bodyAsString().toInt()
                // 此处是vertx的webClient,是一个异步的http client
                // 7777是端口,pi是host(在下部署在树莓派上做的测试),后面是uri
                webClient.get(7777, "pi", "/sub?a=$`m + n`&b=$l").send{
                    if (it.succeeded()) {
                        var `m + n - l` = it.result().bodyAsString().toInt()
                        replyHandler(`m + n - l`.toString())
                    } else {
                        replyHandler("calc error: m + n - l")
                    }
                }
            } else {
                replyHandler("calc error: m + n")
            }
        }
    }
}

上面是一段基于callback的写法,略微有一些回调地狱迷之缩进
那么如果采用promise模式,在本案例的场景下,是可以缓解回调地狱迷之缩进,大家感受一下(此处同样也是使用了vert.x里的promise类):

 override fun start(startPromise: Promise<Void>?) {
    webClient = WebClient.create(vertx)

    var eventBus = vertx.eventBus()
    eventBus.consumer<JsonObject>("calc.promise"){ msg ->
        var msgBody = msg.body()
        var m = msgBody.getInteger("a", 0)
        var n = msgBody.getInteger("b", 0)
        var l = msgBody.getInteger("c", 0)

        add(m, n).future().onSuccess {
            sub(it, l).future().onSuccess {
                msg.reply(it.toString())
            }
        }
    }
}

fun add(a: Int, b: Int) : Promise<Int> {
    var promise = Promise.promise<Int>()

    webClient.get(7777, "pi", "/add?a=$a&b=$b").send{
        if (it.succeeded()) {
            var addResult = it.result().bodyAsString().toInt()
            promise.complete(addResult)
        } else {
            promise.fail("calc failed $a add $b")
        }
    }

    return promise
}

fun sub(a: Int, b: Int) : Promise<Int> {
    var promise = Promise.promise<Int>()

    webClient.get(7777, "pi", "/sub?a=$a&b=$b").send{
        if (it.succeeded()) {
            var addResult = it.result().bodyAsString().toInt()
            promise.complete(addResult)
        } else {
            promise.fail("calc failed $a sub $b")
        }
    }

    return promise
}

上面这种写法是基于promise模式的,虽然有所缓解,但主要功效还是在把一些代码抽离了,核心的计算逻辑,依然还是迷之缩进的,莫着急,后面还有内容。
此处先仔细看下这两段代码,vertx原生提供的io.vertx.core.Promise类,就是用来做promise模式的, 那么promise相比于callback,改变到底是啥?经过日夜揣摩,在下得出结论:

当调用一个异步方法时,promise 模式可以把针对该方法的执行结果的处理逻辑从入参(回调函数),变为返回值;这样带来的编程变化是:基于返回值的处理,更方便的支持链式写法,把缩进改为一条链

这个话题先聊到这里,稍微找一找promise的感觉,咱们接着案例往下聊

案例二

基于计算器服务,实现一个接口: a + ((b -c)+ d) -e -f + g
先看看基于回调怎么写:

//源码地址:https://github.com/HongkaiWen/vertx-kotlin/blob/promise/src/main/kotlin/com/github/hongkaiwen/reactor/vk/calc/CallbackVerticle.kt

class CallbackVerticle : AbstractVerticle(){

    lateinit var webClient: WebClient

    override fun start(startPromise: Promise<Void>?) {
        webClient = WebClient.create(vertx)

        var eventBus = vertx.eventBus()
        eventBus.consumer<JsonObject>("calc.callback"){
            var msgBody = it.body()
            var a = msgBody.getInteger("a", 0)
            var b = msgBody.getInteger("b", 0)
            var c = msgBody.getInteger("c", 0)
            var d = msgBody.getInteger("d", 0)
            var e = msgBody.getInteger("e", 0)
            var f = msgBody.getInteger("f", 0)
            var g = msgBody.getInteger("g", 0)
            calc(a, b, c, d, e, f, g){reply ->
                it.reply(reply)
            }
        }
    }

    fun calc(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, replyHandler: (String) -> Unit){
        webClient.get(7777, "pi", "/sub?a=$b&b=$c").send{
            if (it.succeeded()) {
                var `b-c` = it.result().bodyAsString().toInt()
                webClient.get(7777, "pi", "/add?a=$`b-c`&b=$d").send{
                    if (it.succeeded()) {
                        var `(b-c)+d` = it.result().bodyAsString().toInt()
                        webClient.get(7777, "pi", "/add?a=$a&b=$`(b-c)+d`").send{
                            if (it.succeeded()) {
                                var `a+(b-c)+d` = it.result().bodyAsString().toInt()
                                webClient.get(7777, "pi", "/sub?a=$`a+(b-c)+d`&b=$e").send{
                                    if (it.succeeded()) {
                                        var `a+(b-c)+d-e` = it.result().bodyAsString().toInt()
                                        webClient.get(7777, "pi", "/sub?a=$`a+(b-c)+d-e`&b=$f").send{
                                            if (it.succeeded()) {
                                                var `a+(b-c)+d-e-f` = it.result().bodyAsString().toInt()
                                                webClient.get(7777, "pi", "/add?a=$`a+(b-c)+d-e-f`&b=$g").send{
                                                    if (it.succeeded()) {
                                                        var `a+(b-c)+d-e-f+g` = it.result().bodyAsString().toInt()
                                                        replyHandler(`a+(b-c)+d-e-f+g`.toString())
                                                    } else {
                                                        replyHandler("calc error: a + (b - c) + d -e -f + g")
                                                    }
                                                }
                                            } else {
                                                replyHandler("calc error: a + (b - c) + d -e -f")
                                            }
                                        }

                                    } else {
                                        replyHandler("calc error: a + (b - c) + d -e")
                                    }
                                }
                            } else {
                                replyHandler("calc error: a + (b - c) + d")
                            }
                        }
                    } else {
                        replyHandler("calc error: (b - c) + d")
                    }
                }
            } else {
                replyHandler("calc error: b - c")
            }
        }
    }

}

上面这段代码,实在让人吐血,终于知道啥叫地狱了, 针对上面的方法,基于promise来做改进,效果如何呢:

//源码地址:https://github.com/HongkaiWen/vertx-kotlin/blob/promise/src/main/kotlin/com/github/hongkaiwen/reactor/vk/calc/PromiseVerticle.kt

class PromiseVerticle : AbstractVerticle(){


    lateinit var webClient: WebClient

    override fun start(startPromise: Promise<Void>?) {
        webClient = WebClient.create(vertx)

        var eventBus = vertx.eventBus()
        eventBus.consumer<JsonObject>("calc.promise"){ msg ->
            var msgBody = msg.body()
            var a = msgBody.getInteger("a", 0)
            var b = msgBody.getInteger("b", 0)
            var c = msgBody.getInteger("c", 0)
            var d = msgBody.getInteger("d", 0)
            var e = msgBody.getInteger("e", 0)
            var f = msgBody.getInteger("f", 0)
            var g = msgBody.getInteger("g", 0)

            sub(b, c).future().onSuccess {
                add(it, d).future().onSuccess {
                    add(a, it).future().onSuccess {
                        sub(it, e).future().onSuccess {
                            sub(it, f).future().onSuccess {
                                add(it, g).future().onSuccess {
                                    msg.reply(it.toString())
                                }
                            }
                        }
                    }
                }
            }

        }
    }

    fun add(a: Int, b: Int) : Promise<Int> {
        var promise = Promise.promise<Int>()

        webClient.get(7777, "pi", "/add?a=$a&b=$b").send{
            if (it.succeeded()) {
                var addResult = it.result().bodyAsString().toInt()
                promise.complete(addResult)
            } else {
                promise.fail("calc failed $a add $b")
            }
        }

        return promise
    }

    fun sub(a: Int, b: Int) : Promise<Int> {
        var promise = Promise.promise<Int>()

        webClient.get(7777, "pi", "/sub?a=$a&b=$b").send{
            if (it.succeeded()) {
                var addResult = it.result().bodyAsString().toInt()
                promise.complete(addResult)
            } else {
                promise.fail("calc failed $a sub $b")
            }
        }

        return promise
    }


}

大地狱和小地狱的区别而已,因此就在思忖,貌似使用姿势不对,基于promise的代码依然是迷之缩进的,除非我们可以把promise的处理逻辑做成链式的

JavaScript的promise

感觉上面的路子还是不对,所以我们来参考下前端同学是如何玩promise的;因为在下对JavaScript不感兴趣,所以不求甚解的从大神的博客 摘取一段,如下:

function multiply(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' x ' + input + '...');
        setTimeout(resolve, 500, input * input);
    });
}

// 0.5秒后返回input+input的计算结果:
function add(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' + ' + input + '...');
        setTimeout(resolve, 500, input + input);
    });
}

var p = new Promise(function (resolve, reject) {
    log('start new Promise...');
    resolve(123);
});

p.then(multiply)
 .then(add)
 .then(multiply)
 .then(add)
 .then(function (result) {
    log('Got value: ' + result);
});

发现JavaScript里的promise是链式的(就应该这样嘛),那么上面的例子里的姿势一定是错了,问题在哪呢?
JavaScript里的用法,promise是通过then函数进行顺序链式条用的,那么这个then函数,输入是一个处理函数,返回的是一个新的promise才行,这样可以针对一环一环的处理作出管道的效果来。
因为我采用的vertx的promise来做研究,这个类本身处理promise的结果的方法 onSuccess返回的是future本身,并不是一个新的promise的future,所以不好做链式调用:

  @Fluent
  default Future<T> onSuccess(Handler<T> handler) {
    return onComplete(ar -> {
      if (ar.succeeded()) {
        handler.handle(ar.result());
      }
    });
  }

改进方案

仔细看了一圈io.vertx.core.Promise的相关方法, 发现compose正是我们的解药。

class PromiseLineVerticle3 : AbstractVerticle(){


    lateinit var webClient: WebClient

    override fun start(startPromise: Promise<Void>?) {
        webClient = WebClient.create(vertx)

        var eventBus = vertx.eventBus()
        eventBus.consumer<JsonObject>("calc.promise.line3"){ msg ->
            var msgBody = msg.body()
            var a = msgBody.getInteger("a", 0)
            var b = msgBody.getInteger("b", 0)
            var c = msgBody.getInteger("c", 0)
            var d = msgBody.getInteger("d", 0)
            var e = msgBody.getInteger("e", 0)
            var f = msgBody.getInteger("f", 0)
            var g = msgBody.getInteger("g", 0)

            //只要一个onFailure即可
            (b asyncSub c)
                .compose { it asyncAdd d }
                .compose { it asyncAdd a }
                .compose { it asyncSub e }
                .compose { it asyncSub f }
                .compose { it asyncAdd g }
                .onSuccess { msg.reply(it.toString()) }
                .onFailure{msg.fail(500, it.message)}
        }
    }

    infix fun Int.asyncAdd(input : Int) : Future<Int> {
        return calc(this, input, CalcOperator.add)
    }

    infix fun Int.asyncSub(input : Int) : Future<Int> {
        return calc(this, input, CalcOperator.sub)
    }

    /**
     * 所有异常必须被处理
     */
    fun calc(a: Int, b: Int, operator: CalcOperator) : Future<Int> {
        var promise = Promise.promise<Int>()

        webClient.get(7777, "pi", "/${operator.name}?a=$a&b=$b").send{
            if (it.succeeded()) {
                try{
                    var addResult = it.result().bodyAsString().toInt()
                    promise.complete(addResult)
                    println("$a - $b = $addResult")
                } catch (e: Exception) {
                    promise.fail(e)
                }
            } else {
                it.cause().printStackTrace()
                promise.fail("calc failed $a add $b")
            }
        }

        return promise.future()
    }
}

enum class CalcOperator {
    add, sub
}

通过compose函数代码清爽了许多。
特别要提一下的是,上面的写法it asyncAdd d是kotlin的中缀表达式,另外这个地方的it,java程序员一定会问怎么没有声明过就使用?对的,kotlin中的lambda如果只有一个参数,就可以默认用it代替,写法简单而且避免不知道该其起啥变量名。

基于CompletableFuture实现promise

因为笔者最近vert.x用的多,各种例子都是基于vert.x的;但其实如果把java和promise一起做搜索的话,得到的结果一般可能是jdk的CompletableFuture实现;而且vert.x社区也有roadmap准备向jdk的CompletionStage靠拢(RFC),所以再写一个CompletableFuture的实现版本:

class PromiseLineVerticle4 : AbstractVerticle(){


    lateinit var webClient: WebClient

    override fun start(startPromise: Promise<Void>?) {
        webClient = WebClient.create(vertx)

        var eventBus = vertx.eventBus()
        eventBus.consumer<JsonObject>("calc.promise.line4"){ msg ->
            var msgBody = msg.body()
            var a = msgBody.getInteger("a", 0)
            var b = msgBody.getInteger("b", 0)
            var c = msgBody.getInteger("c", 0)
            var d = msgBody.getInteger("d", 0)
            var e = msgBody.getInteger("e", 0)
            var f = msgBody.getInteger("f", 0)
            var g = msgBody.getInteger("g", 0)


            (b asyncSub c)
                .thenCompose { it asyncAdd d }
                .thenCompose { it asyncAdd a }
                .thenCompose { it asyncSub e }
                .thenCompose { it asyncSub f }
                .thenCompose { it asyncAdd g }
                .thenAccept { msg.reply(it.toString()) }
                .exceptionally {
                    msg.fail(500, it.message)
                    null
                }
        }
    }



    infix fun Int.asyncAdd(input : Int) : CompletableFuture<Int> {
        return calc(this, input, CalcOperator.add)
    }

    infix fun Int.asyncSub(input : Int) : CompletableFuture<Int> {
        return calc(this, input, CalcOperator.sub)
    }

    /**
     * 所有异常必须被处理
     */
    fun calc(a: Int, b: Int, operator: CalcOperator) : CompletableFuture<Int> {
        var promise = CompletableFuture<Int>()

        webClient.get(7777, "pi", "/${operator.name}?a=$a&b=$b")
            .expect(ResponsePredicate.SC_OK).send{
            if (it.succeeded()) {
                try{
                    var addResult = it.result().bodyAsString().toInt()
                    promise.complete(addResult)
                    println("$a - $b = $addResult")
                } catch (e: Exception) {
                    promise.completeExceptionally(e)
                }
            } else {
                it.cause().printStackTrace()
                promise.completeExceptionally(RuntimeException("calc failed $a add $b"))
            }
        }

        return promise
    }
}

其实无论是vert.x的promise还是jdk的CompletableFuture, 都是promise模式,写发生也比较类似,代码整洁度上也相似,当然笔者很乐于看到java生态圈能够对此形成标准,就和JS的ES6统一定义了前端的promise一样,免得很多口水。

实践上升理论

基于上面的实践,相信对promise是有一点感觉的了。那么需要实践上升理论了,理论参见:
https://en.wikipedia.org/wiki/Futures_and_promises
理论说明相关文章很多,笔者理解主要是状态机的维护,笔者就不再献丑了,后续如果得空在写一篇vert.x的promise源码分析的文章。

promise是异步编程的一个模式,在某些场景下可以解决回调地狱的问题。
接下来的一篇文章讲来聊聊reactor模式。

系列文章快速导航:
异步编程一:异步编程的魅力
异步编程二:promise模式
异步编程三:reactor模式
异步编程四:协程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容