有了异步i/o的话,还需要协程么

1、异步 I/O 和协程区别

这个其实触及了高并发架构的底层原理:

“异步 I/O 和协程有什么区别?如果我已经用异步 I/O(如 NIO、Netty、epoll),还需要协程吗?”

我们来一步步拆开讲清楚(这题很多人理解偏差)👇


一、先看本质区别:异步 I/O vs 协程

特性异步 I/O(如 Netty、epoll、aio)协程(Coroutine,如 Kotlin Coroutine、Go、Project Loom)
层级 属于 系统级 I/O 模型 属于 语言级并发模型
解决问题 避免线程阻塞,提高 I/O 利用率 避免“回调地狱”,让异步代码看起来像同步
底层线程 少量线程(I/O Reactor 模型) 每个协程共享线程,轻量调度
切换成本 系统线程切换(昂贵) 协程上下文切换(纳秒级)
可维护性 回调嵌套多(Callback Hell) 顺序写法(同步风格)
示例框架 Netty、libevent、aio Kotlin Coroutine、Go routine、Java Loom

✅ 举个例子说明区别:

1️⃣ 异步 I/O 版本(以 Java Netty 为例)

httpClient.sendAsync(request, response -> {process(response);
});

看起来简单,但如果链路里有多个异步调用:

httpClient.sendAsync(req1, resp1 -> {httpClient.sendAsync(req2, resp2 -> {httpClient.sendAsync(req3, resp3 -> {...});});
});

就会陷入回调地狱,逻辑很难维护。


2️⃣ 协程版本(以 Kotlin Coroutine 为例)

suspend fun processRequests() {val resp1 = httpClient.get("url1")val resp2 = httpClient.get("url2")val resp3 = httpClient.get("url3")println(resp1 + resp2 + resp3)
}

虽然底层依然是 异步 I/O,但上层通过 协程调度器 挂起/恢复,让你写出同步代码风格,性能一样高。


二、那问题来了:

“如果我已经有异步 I/O,比如用 Netty 了,还需要协程吗?”

答案是:取决于你的目标

目标是否需要协程
仅仅追求高性能(少线程高并发) ❌ 不一定需要,Netty / epoll 已够用
追求高可维护性(业务逻辑多层异步) ✅ 很推荐协程
有复杂调用链(多 HTTP/RPC 并行) ✅ 协程能简化并发逻辑
系统已在使用回调异步框架 ✅ 可用协程封装异步接口

📘 示例:在高并发 HTTP 调用中,异步 I/O + 协程可以配合

案例:电商系统聚合多个下游服务

假设你要并发请求:

  • 商品详情接口

  • 价格接口

  • 库存接口

(1) 纯异步 I/O(如 Java CompletableFuture)
CompletableFuture<Product> p1 = productService.getDetailAsync(id);
CompletableFuture<Price> p2 = priceService.getPriceAsync(id);
CompletableFuture<Stock> p3 = stockService.getStockAsync(id);CompletableFuture.allOf(p1, p2, p3).join();

性能很好,但代码层层嵌套,一旦依赖关系复杂,异常链极难处理。

(2) 协程(如 Kotlin Coroutine)
coroutineScope {val product = async { productService.getDetail(id) }val price = async { priceService.getPrice(id) }val stock = async { stockService.getStock(id) }combine(product.await(), price.await(), stock.await())
}

👉 底层仍然是异步 I/O
👉 但写法像同步一样,异常处理、超时控制都更自然。


三、为什么有了异步 I/O,还会想引入协程?

主要是以下几点:

  1. 协程是对异步编程的“语法抽象”,不是对 I/O 模型的替代;

    • 它并不改变 I/O 的本质(仍然是 epoll/kqueue),

    • 而是改变你 怎么写 代码。

  2. 协程带来更好的开发体验:

    • 异常栈清晰;

    • 顺序写逻辑;

    • 兼容旧的同步接口;

    • 可并发可挂起,调度轻量。

  3. 协程与异步 I/O 结合最完美:

    • 典型如:Kotlin + Netty、Go + epoll、Rust + async/await。


四、总结一句话:

异步 I/O 是“发动机”,让系统能高并发地运行。
协程 是“变速箱”,让开发者能顺畅地操控异步逻辑。

二者不是替代关系,而是:

异步 I/O 提供性能,协程提供可读性与开发效率。

2、实战演练(以jdk21为例)

下面我来展示 Java Loom(JDK 21 虚拟线程) 在 HTTP 异步调用中的 Java 代码示例(即用虚拟线程替代传统异步模型),然后再给一个 Apache Dubbo 异步调用改成协程风格/虚拟线程模型 的示例。这样你面试时可以说明“有异步 I/O + 协程/虚拟线程”的实际落地方式。


一、HTTP 异步调用 + 虚拟线程示例(Java 21)

假设你有一个服务 A,要调用两个外部 HTTP 接口(比如库存服务、价格服务),并行获取结果后再组合返回。传统用异步 CompletableFuture,现在用虚拟线程把代码写成同步风格,但底层还是异步 I/O。

传统 CompletableFuture 异步模型代码

HttpClient httpClient = HttpClient.newHttpClient();CompletableFuture<String> f1 = httpClient.sendAsync(HttpRequest.newBuilder(URI.create("https://api.price/123")).GET().build(),HttpResponse.BodyHandlers.ofString()
).thenApply(HttpResponse::body);CompletableFuture<String> f2 = httpClient.sendAsync(HttpRequest.newBuilder(URI.create("https://api.stock/123")).GET().build(),HttpResponse.BodyHandlers.ofString()
).thenApply(HttpResponse::body);String result = CompletableFuture.allOf(f1, f2).thenApply(v -> {try {return "price=" + f1.get() + ", stock=" + f2.get();} catch (Exception e) {throw new RuntimeException(e);}}).get();

代码较复杂,有回调 + future 链。


使用虚拟线程 (JDK 21) 的代码

import java.net.http.*;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class VirtualThreadHttpExample {public static void main(String[] args) throws Exception {// 创建执行器:每个任务一个虚拟线程try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).executor(executor)  // 使用虚拟线程执行器(可选,根据场景)
                .build();String result = executor.submit(() -> {String price = httpClient.send(HttpRequest.newBuilder(URI.create("https://api.price/123")).GET().build(),HttpResponse.BodyHandlers.ofString()).body();String stock = httpClient.send(HttpRequest.newBuilder(URI.create("https://api.stock/123")).GET().build(),HttpResponse.BodyHandlers.ofString()).body();return "price=" + price + ", stock=" + stock;}).get();System.out.println("Result: " + result);}}
}

说明:

  • Executors.newVirtualThreadPerTaskExecutor() 创建的是虚拟线程执行器。虚拟线程是轻量的,不像传统线程绑定 OS 线程。Oracle 文档+1

  • 在这个任务里,httpClient.send(...) 是同步调用,但因为线程是虚拟线程,它在等待 I/O 时不会占用一个 OS 线程资源,而是被挂起,底层 carrier 线程可复用。

  • 写代码时看起来像同步,逻辑顺序清晰;底层实现仍是并发、多任务执行。

  • 相比用 sendAsync() + CompletableFuture,可读性更好。

在面试中要点:

  • 强调“虚拟线程让你以同步风格写并发”

  • 强调“底层仍发挥 I/O 并发优势”

  • 提及“线程数量不再被 OS 限制,可支持数万虚拟线程”Medium+1

  • 注意:不要把 CPU-密集任务放虚拟线程,因为那样无益。


二、Dubbo 异步调用改成虚拟线程/协程风格示例

假设你有一个游戏商城服务,用 Dubbo 调用了远程账户服务 AccountService 和库存服务 InventoryService,原来使用 Dubbo 的异步 RPC(CompletableFuture)形式,现在改成虚拟线程运行。

传统 Dubbo 异步调用(伪代码)

@Service
public class OrderService {@DubboReferenceprivate AccountService accountService;@DubboReferenceprivate InventoryService inventoryService;public OrderResult placeOrder(OrderRequest req) throws Exception {CompletableFuture<AccountInfo> f1 = accountService.getAccountAsync(req.getUserId());CompletableFuture<InventoryInfo> f2 = inventoryService.getInventoryAsync(req.getSkuId());// 组合后逻辑return CompletableFuture.allOf(f1, f2).thenApply(v -> {AccountInfo acct = f1.join();InventoryInfo inv = f2.join();// further logicreturn new OrderResult(...);}).get();}
}

改为虚拟线程执行的版本

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@Service
public class OrderService {@DubboReferenceprivate AccountService accountService;@DubboReferenceprivate InventoryService inventoryService;// 虚拟线程执行器(可注入)private final ExecutorService vtExecutor = Executors.newVirtualThreadPerTaskExecutor();public OrderResult placeOrder(OrderRequest req) throws Exception {return vtExecutor.submit(() -> {// 现在可以用同步调用接口(假定 Dubbo 支持同步调用)AccountInfo acct = accountService.getAccount(req.getUserId());InventoryInfo inv = inventoryService.getInventory(req.getSkuId());// 下单、校验、扣库存、扣款逻辑//return new OrderResult(...);}).get();}
}

说明:

  • 如果 accountService.getAccount()inventoryService.getInventory() 是阻塞型调用(同步 RPC),虚拟线程也可以直接调用,代码更简洁。

  • 如果 Dubbo 库仍是异步形式,也可以在虚拟线程中调用 future.join();因为虚拟线程挂起期不会占用 OS 线程。

  • 这种改法减少了异步回调/链式 CompletableFuture 代码复杂度,提高可读性与维护性。

在面试中要点:

  • 提及“将 RPC 异步回调 + Future 组合模式改为虚拟线程 + 同步调用模式”

  • 强调“业务逻辑变得线性,异常处理也更简单”

  • 强调“在高并发 RPC 场景下,虚拟线程让我们不用担心 thread pool 泄漏、连接堵塞”

  • 同时说明依然要注意“如果 RPC 底层仍是同步阻塞(如 JDBC、某些驱动)可能会 pin OS 线程”Stack Overflow+1


三、面试时完整回答结构(结合上面两个示例)

“在我们的海外支付/游戏商城项目中,我采用了 JDK 21 的虚拟线程(Project Loom)来简化并发逻辑。
比如:在调用两条外部 HTTP 接口(价格服务 + 库存服务)时,我原来用 CompletableFuture 链式组合。但改为虚拟线程后,只需:

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {// 同步风格调用String price = httpClient.send(...).body();String stock = httpClient.send(...).body();...
}

这样代码更直观、维护成本更低,而且虚拟线程的调度机制使得数万个并发请求也能高效支持。

再如在游戏商城中,我将多个 Dubbo RPC 异步调用改为由虚拟线程执行同步接口:

vtExecutor.submit(() -> {AccountInfo acct = accountService.getAccount(...);InventoryInfo inv = inventoryService.getInventory(...);...return result;
}).get();

这样避免了未来链 (CompletableFuture) 嵌套,提高了可读性。

在落地时我们也注意以下要点:

  • 确保底层是 I/O 绑定场景(虚拟线程最适合 I/O 多、CPU 少的任务)Stack Overflow

  • 避免同步阻塞如 synchronized 导致 carrier 线程 pinning 问题。Stack Overflow

  • 监控线程数、task延迟、资源利用情况。

结果:我们从原先线程池最大 2000 个平台线程切换到虚拟线程后,在同样硬件下并发数提升约 3-5 倍,代码逻辑简化约 30%。”

3、原理分析

协程(或 Java Loom 的虚拟线程)底层调度与阻塞 I/O 的核心原理 —— 这恰恰是“为什么协程能既看起来同步、又不会真的阻塞线程”的关键。
下面我们分两部分来详细解释:


一、协程如何处理“同步 I/O 调用”而不真正阻塞线程

1️⃣ 普通线程的阻塞模型

在传统 Java 中,一个线程调用阻塞 I/O(例如 Socket.read()HttpClient.send())时:

  • 这个线程会一直等待内核 I/O 完成;

  • 直到系统调用返回数据;

  • 在等待期间,这个线程会占用一个 OS 线程(Kernel Thread),也就是说 CPU 无法复用它。

这就是为什么传统线程模型需要:

“I/O 多的场景,要么用异步 I/O,要么用线程池控制线程数量,否则线程太多会爆栈或调度开销大”。


2️⃣ 虚拟线程(协程)的不同之处 — “可挂起的同步调用”

Java 21 的虚拟线程(Project Loom)在 JVM 层做了重大改造:

当一个虚拟线程执行到一个可能阻塞的系统调用时(例如 Socket I/O、文件 I/O 等),JVM 会检测到并执行以下动作:

  1. 挂起当前虚拟线程(park)
    虚拟线程栈帧保存到堆(heap stack),不再绑定底层 OS 线程;

  2. 释放其底层载体线程(carrier thread)
    OS 线程回到 ForkJoinPool 或虚拟线程调度器中,可以继续执行别的虚拟线程;

  3. 当 I/O 完成时,操作系统通过事件通知(epoll/kqueue/IOCP),
    JVM 恢复对应虚拟线程的栈帧;

  4. 调度器将虚拟线程重新分配给一个可用 OS 线程继续执行。

👉 这样,“看似阻塞”的同步代码,实际上在底层是非阻塞、可挂起恢复的。

总结一句话:

虚拟线程通过「可挂起的栈帧 + 调度器重绑 carrier 线程」实现了同步代码的异步执行。


3️⃣ 举例对比流程图

场景普通线程虚拟线程
调用 socket.read() OS 线程阻塞 虚拟线程挂起,OS 线程释放
I/O 等待期间 OS 线程闲置 OS 线程执行别的任务
I/O 完成 唤醒原线程继续 恢复虚拟线程栈并重新执行

4️⃣ 关键点:协程是运行时支持的“轻量可挂起任务”

虚拟线程 = JVM 级协程。
它的可挂起是JVM 透明支持的,而不是靠用户写 awaityield
这和 Kotlin、Go、Rust 的异步模型原理类似,只是实现层级不同:

  • Kotlin 协程靠编译器插入 suspend 状态机;

  • Go 协程靠 runtime scheduler;

  • Java 虚拟线程靠 JVM HotSpot 层直接调度。


二、是不是所有 I/O 都能自动“非阻塞化”?

不完全是。👇

✅ 能自动挂起的情况:

JDK 21 已经为虚拟线程改造了大量 JDK 类,使它们支持挂起:

  • 所有基于 java.net.Socket / HttpClient / NIO 的网络 I/O;

  • 文件 I/OFileChannelFiles.newBufferedReader() 等);

  • 锁、同步等待(如 LockSupport.park()Object.wait());

  • 阻塞队列、Semaphore 等并发类(内部也做了适配)。

这些都能让虚拟线程在阻塞时挂起,不占用 OS 线程。

🔍 JVM 内部有个关键类:

jdk.internal.vm.Continuation
负责保存/恢复虚拟线程的栈帧。
当检测到阻塞点(例如 SocketInputStream.read),会调用 Continuation.yield() 触发挂起。


❌ 不能自动挂起的情况:

有一些 JNI(native)调用或三方库 I/O,JVM 无法感知其阻塞状态:

  • 使用 JDBC 驱动(例如 MySQL、Oracle)时,驱动内部是 native socket 阻塞;

  • 使用 Redis、Mongo、Kafka 等客户端,如果底层不是基于 JDK NIO,也会导致“线程 pinning”;

  • 调用外部 native 方法或 C 库。

在这些情况下:

  • 虚拟线程无法挂起;

  • 会“pin”住底层 OS 线程(carrier thread);

  • 导致虚拟线程的优势丧失。

📌 JVM 文档称这种情况为 “carrier thread pinning”
JEP 444 明确建议驱动厂商改造为支持 Loom 的异步 I/O。


解决方案 / 最佳实践

场景最佳方式
HTTP / gRPC 用 JDK HttpClient 或 Netty + Loom
JDBC 等待支持 Loom 的驱动(MySQL Connector 8.2+ 已部分支持)
Redis / Kafka 使用 Lettuce / Reactor / Netty 驱动版本(或 Loom-safe wrapper)
混合场景 对非支持的 I/O 仍用普通线程池分发,避免 carrier pinning

三、面试时总结回答模板

“协程或虚拟线程不会凭空消除阻塞,它只是通过在运行时挂起任务、释放 OS 线程资源来实现高并发。
对于 JDK 自带的 I/O,比如 Socket、HttpClient,这种挂起是 JVM 层自动实现的,真正达到‘看似同步、实则异步’。
但对于一些三方驱动(如 JDBC、Redis)如果底层是 native 阻塞调用,那虚拟线程仍然会 pin 住 OS 线程。
所以在实际项目中,我们会选择 Loom-兼容 I/O 库,或者将阻塞调用单独放入普通线程池。
这样既能保留同步代码的可读性,又能发挥异步 I/O 的性能优势。”


 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/940716.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

永久暂停window10更新,不想更新到window11

视频:https://www.bilibili.com/video/BV1jsTMz9EUz?t=144.0 饱受自动更新之苦,现提供一个3分钟可以“永久”关闭Windows更新的思路。具体步骤: 1、Win+R,regedit打开注册表编辑器; 2、找到路径HKEY_LOCAL_MACHI…

102302148谢文杰第一次数据采集作业

第一题 核心代码与运行结果点击查看代码 import requests from bs4 import BeautifulSoup# 目标URL:2020年中国大学排名页面 url="http://www.shanghairanking.cn/rankings/bcur/2020" response=requests.g…

算法第二章作业

找第 k 小的数的分治算法自然语言描述: 找第 k 小的数的分治算法,首先要选择一个基准元素,然后将数组分成两部分,一部分是小于等于基准元素的数,另一部分是大于基准元素的数。假设基准元素在划分后位于数组的第 m…

完全免费的 claude 工具,真香!

完全免费的 claude 工具,真香!刚把我这段时间用 puter 搭的一个 Claude 对话小工具上线了,免费的。 它不是那种“神乎其神”的产品,但确实帮我写方案、理思路、看代码,节省了很多来回搜索的时间。 登录即可使用,…

RaspberryPi 个人服务搭建

# RaspberryPi 个人服务搭建 > 树莓派及其他debian衍生版本服务> > 版本:Linux debian 6.12.48+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.48-1 (2025-09-20) x86_64 GNU/Linux> > 日期:2025年…

tryhackme-预安全-网络如何工作-网站如何工作-11

tryhackme-Pre Security-How The Web Works-How Websites Work 房间地址:https://tryhackme.com/room/howwebsiteswork 这是网络安全入门的基础模块的计算机科学基础知识:How Websites Work(网站如何工作),序号 0…

2025塑料托盘优质厂家推荐,力森塑业科技多元化产品满足各类需求!

2025塑料托盘优质厂家推荐,力森塑业科技多元化产品满足各类需求!随着物流仓储行业的快速发展,塑料托盘作为重要的物流工具,其市场需求日益增长。然而,当前塑料托盘领域面临着诸多技术挑战,这些问题不仅影响了产品…

嵌入式实验3串口通信--任务二USART1通信

1)STM32系统给上位机(win10)连续发送“hello windows!”,win10采用“串口助手”工具接收。如果STM32的连续发送之间不加延时语句,观察win10端是否出现接收数据丢失的现象。 1.1在STM32CubeMX中建立一个新的工程。…

[SSH] sftp 基于SSH的交互式文件传输工具

[SSH] sftp 基于SSH的交互式文件传输工具$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");目录01 背景1.1 简介1.2 sftp 与 ftp 的对比1.3 sftp 与 scp 适用场…

java.math 包详解

java.math 包详解java.math 包是 Java 提供的用于高精度数学计算的工具包,主要包含两个核心类:BigInteger 和 BigDecimal。这些类用于处理超出基本数据类型范围的数值运算。 1. BigInteger 类BigInteger 用于表示任意…

Drive Snapshot

Drive SnapshotAcronis True ImageATI老版本非常棒

Python接入A股level2千档盘口和逐笔委托

Python接入A股level2千档盘口和逐笔委托本文将以实际的代码实践为例,探讨如何通过一套集成了A股基础行情、Level-2高速行情WebSocket接口、量化交易接口与金融数据库的服务,来系统性地解决这些问题。你将看到如何用简…

20232319 2025-2026-1 《网络与系统攻防技术》实验二实验报告

一、实验内容 1.实践目标 (1)使用netcat获取主机操作Shell,cron启动某项任务(任务自定) PS:cron是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程 (2)使用socat获取主机操作Shell, 任务计划启动…

刷题日记—洛谷循环题单

1.数学思想在算法题中的应用: 2.回文数的判定:

学号 2025-2026-1 《网络与系统攻防技术》实验二实验报告

一、实验内容 1.实践目标 (1)使用netcat获取主机操作Shell,cron启动某项任务(任务自定) PS:cron是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程 (2)使用socat获取主机操作Shell, 任务计划启动…

为什么需要学习变异的算法?

对,“金标准得到的时间很长” 是一个非常现实且关键的点,尤其在临床或产品落地场景中,时间成本本身就是限制模型迭代和优化的瓶颈。✅ 补充第6点(在你这个设定下):障碍类别 具体原因 影响金标准延迟 金标准(如S…

今天搞了新的回归,不显著

今天搞了新的回归,可能被解释变量的度量存在问题,也可能是解释变量有毛病,总之回归结果跟预期的正好相反,甚至反的很显著,这也太恶心了。

shell编程学习笔记005之until循环

until循环语句各种用法探索语法: until 条件 do执行; done

shell编程学习笔记006之select循环

select循环语句各种用法探索语法: select 变量 in "列表" dolist done说明: select可以显示带编号的菜单,选择并执行不同的功能。 select是死循环,可以通过break语句终止循环,也可以使用Ctrl+D或Ctrl+C…

burpsuite抓取小程序公众号数据包-cnblog

burpsuite抓取小程序 公众号https数据包 burpsuite官网下载 https://portswigger.net/burp破解工具 通过网盘分享的文件:burp破解工具.zip 链接: https://pan.baidu.com/s/1An6ebL2LdM3VvFhJSRf4Zg?pwd=8888 提取码…