6.3 Future 与 Promise:异步结果的单据


6.3 Future 与 Promise:异步结果的单据

本节摘要:Future 是"取结果的单据",Promise 是"填结果的单据"。Netty 的 ChannelFuture 扩展了 JDK Future 的监听能力,Promise 则让任意异步操作都能挂进同一套体系。本节讲清两者的关系、addListener 组合写法,以及"谁在哪个线程收货"的判断方法。

一、从一次 connect 说起

第一章写过 b.connect(...).sync()——sync 会阻塞等到连接结果。但 EventLoop 线程里绝不能这么等(6.1 的自等死锁),所以要换监听风格:

ChannelFuture cf = b.connect("127.0.0.1", 8080); cf.addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { System.out.println("连接成功:" + future.channel()); future.channel().writeAndFlush("hello\n"); } else { System.err.println("连接失败:" + future.cause().getMessage()); } }); // 主线程立刻往下走,不等

这里的 ChannelFuture 就是一张单据:connect 这个异步操作开工时给你开一张,结果出来前单据是"未完成"态,出来后变"成功"或"失败",监听器在状态翻转的那一刻被回调——回调发生在哪个线程?答案是与单据关联的 EventLoop 线程(对 connect 来说是客户端 EventLoop)。

JDK 的 Future 有两个先天不足:get 只能阻塞轮询,且 cancel 之外没有完成通知。Netty 的扩展正中要害:

能力 JDK Future Netty ChannelFuture
阻塞获取 get(抛检查异常) sync / await(区分抛与不抛)
完成通知 addListener 回调
失败原因 get 时抛出 cause 直接可查
关联对象 channel 一并持有

二、Promise:能主动填结果的单据

Future 是只读单据,Promise 是它的可写面——Netty 里 ChannelPromise 既是出站操作的回执,也能拿来包装任意异步操作:

// 把"查数据库"包装成 Netty 风格的异步单据 public Future<User> fetchUserAsync(long uid, EventExecutor executor) { Promise<User> promise = new DefaultPromise<>(executor); bizExecutor.execute(() -> { try { User u = db.queryUser(uid); // 慢活 promise.setSuccess(u); // 填单:成功 } catch (Exception e) { promise.setFailure(e); // 填单:失败 } }); return promise; // 单据立刻可发,先用后填 } fetchUserAsync(9527, ctx.executor()).addListener(f -> { if (f.isSuccess()) { ctx.writeAndFlush(((Future<User>) f).getNow().getName()); // 注意:getNow 不阻塞,单据已完成才有值 } else { ctx.close(); } });

setSuccess/setFailure 只能调一次,第二次调抛 IllegalStateException——单据不能改口。这个"一次性"正是 Promise 语义的核心:完成即定局,监听器要么已通知要么即将通知,绝无竞态

一张单据的状态流转

三、组合:把回调金字塔压平

监听器里再发起异步操作,一层套一层就是"回调地狱"。压平的办法是把每步都变成单据传递,用 Netty 的 GlobalEventExecutor(或业务线程池)串起来:

// 目标:登录 → 拉资料 → 预扣额度,三步全异步且不嵌套 Promise<Session> loginPromise = loginAsync(userId, password, ctx.executor()); loginPromise.addListener((FutureListener<Session>) f -> { Session s = f.getNow(); fetchUserAsync(s.getUid(), ctx.executor()) .addListener((FutureListener<User>) fu -> { reserveAsync(fu.getNow(), ctx.executor()) .addListener(rf -> ctx.writeAndFlush(rf.isSuccess() ? "OK" : "FAIL")); }); });

嵌套仍有一层,但每层都是"单据进、单据出",没有阻塞、没有自等。更深的组合建议引入 CompletableFuture 做胶水:NettyFutureCompletableFuture 之间用 listener 一行互转,后者提供 thenCompose 的扁平组合能力——两套体系各取所长。

四、写操作的回执与刷写确认

writeAndFlush 返回的也是 ChannelFuture,出站操作完成(写进系统发送缓冲)时触发。两类典型用法:

// 用法一:写完就关——短连接回包的标准收尾 ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE); // 用法二:确认发出后做统计/重试 ctx.writeAndFlush(msg).addListener(f -> { if (!f.isSuccess()) { log.warn("发送失败 准备重连 {}", f.cause().getMessage()); f.channel().close(); } });

注意"写完成"指字节进了内核发送缓冲,不等于对方收到——需要应用层 ACK 时,协议要自带确认消息,传输层单据回答不了这个问题。

⚠️ 常见坑三连:在 EventLoop 线程里 sync/await 自家单据(死锁,6.1 讲过);listener 里再调 get() 阻塞(listener 已在完成态,getNow 才是正确姿势);Promise 忘了 setFailure,调用方单据永远悬挂——每个异步分支的异常出口都必须填单。

💡 关键直觉:把异步操作都想成"开单—干活—填单"三步:调用方拿单先行,干活的线程填单定局,监听器收单续作。判断任何 Future 代码的正确性,就看填单路径是否覆盖了所有出口(成功、失败、超时)。

单据守则速记

  • Future 与 Promise:读单据与写单据一体两面,完成一次性定局。
  • addListener 代替 get:EventLoop 线程只允许监听式等待。
  • 回调线程归属:监听器跑在单据关联的 EventExecutor 上。
  • getNow 与 cause:完成态下的非阻塞取值与取因。
  • 组合姿势:单据串单据压平嵌套,深组合借助 CompletableFuture 互转。
  • write 的回执语义:进内核即完成,业务确认要靠协议 ACK。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U