Vert.x 是一个用于构建反应式应用程序的工具包,它为多种 JVM 语言提供了支持,包括 Java、Kotlin、Groovy 和 Scala。对于 Kotlin 用户而言,Vert.x 提供了额外的支持和扩展,使得使用 Kotlin 编写 Vert.x 应用更加自然和高效。
通过利用 Kotlin 的语言特性,如协程(coroutines),开发者可以编写异步代码,就像它是同步的一样,同时保持非阻塞和高性能的特性。为了在 Kotlin 中充分利用 Vert.x 的功能,我们需要引入特定的依赖项,并遵循一些最佳实践来确保我们的应用能够以最有效的方式运行。
Vert.x 不是作为一个框架而是一个工具包是因为他的侵入性不强,甚至可以和 springboot 一起使用。但是这里仅仅讨论 vertx 使用协程的情况,因此默认你拥有了 vertx 的基础知识
Vertx 使用协程可以很轻松的做到不阻塞事件循环线程,并且可以像写同步代码一样写异步代码。很巧妙的规避了 Vertx 的回调地狱的问题。文章最后有 Java 和 Kotlin 协程 对比
所需依赖
vertx-lang-kotlin
vertx-lang-kotlin-coroutines
一、开启协程
- 获取 Vertx 提供的事件循环协程调度器
val vertxDispatcher = vertx.dispatcher()
- 使用调度器创建协程,该协程调度器和
Dispatchers.IO
等调度器使用方法一致
需要注意的是,该协程使用是 EventLoop 生成的调度器,所以同样和使用 EventLoop 一样不能使用阻塞代码
CoroutineScope(vertx.dispatcher()).launch {
delay(1000)
}
- Vert.x 还提供了一个返回io.vertx.core.Future实例的协程构建器。
val future1: Future<String> = vertxFuture(vertx) {
//...
"Result"
}
- 定义子协程
CoroutineScope(vertx.dispatcher()).launch {
delay(1000)
coroutineScope{
delay(1000)
}
}
注意事项
- 避免阻塞代码:正如上面提到的,当使用 vertxDispatcher 创建的协程时,必须避免任何可能会阻塞事件循环线程的操作。如果你确实需要执行阻塞操作,考虑使用 awaitBlocking 或者切换到另一个适合执行阻塞任务的调度器,例如 Dispatchers.IO。
- 资源管理:确保正确管理协程作用域的生命周期。比如,在适当的时候取消 Coroutine,以防止潜在的内存泄漏或不必要的后台工作。
- 禁止在 Vert.x 事件循环线程中使用runBlocking。也同样在Vertx 的协程中禁止使用 runBlocking
- 为了避免内存泄漏,请始终使用
coroutineScope {..}
定义子范围。这样,如果范围内的协程失败,则在该范围内定义的所有其他协程也将被取消。
二、辅助函数
awaitEvent
, awaitResult
, 和 awaitBlocking
是用于简化异步编程的辅助函数
awaitEvent
等待一个事件发生
异步执行阻塞代码,直到阻塞到事件发生
执行该代码后会挂起当前协程,并等待 awaitEvent 执行完毕
val s = awaitEvent { handler ->
// 协程会被阻塞,直到事件发生,这个动作不会阻塞 vertx 的事件循环。
// 只要将 awaitEvent 的入参 handler 执行即可
server.setTimer(1000, handler)
}
awaitResult
等待结果返回
异步运行阻塞并等待结果。
使用 awaitResult 可以等待具有Handler<AsyncResult>
返回值的方法代码执行完毕,并最后调用handler
恢复挂起的当前协程 。通常是将handler
作为参数传入,如代码所示
onCompletion
awaitResult 返回完成 future 的值onFailure
awaitResult 抛出 future 失败的异常
// 这个处理程序可以传递给 Vert. x 异步方法
// 协程将被阻塞,直到 future 完成或失败,此操作不会阻塞 vertx 的事件循环。
val s = awaitResult { handler ->
server.listen(8080, handler)
}
awaitBlocking
阻塞操作代码
和
vertx.executeBlocking
不同的是,executeBlocking
是异步执行的不会等待结果返回,awaitBlocking
则是挂起当前协程等待结果返回
在 worker 线程上运行 asynchronous block 并等待结果。
执行阻塞代码并最后返回一个结果或执行失败引发异常。
执行结束,返回执行结果
执行结束,如果有异常就直接抛出
val s = awaitBlocking {
// 模拟阻塞代码。
// worker 线程上运行的因此不允许使用delay挂起
Thread.sleep(1000)
"返回结果"
}
顺便给一个关于
vertx.executeBlocking
的拓展函数封装
/**
*
* @param ordered 如果为 true,则如果在同一上下文中多次调用 executeBlocking,则该上下文的执行将按顺序执行,而不是并行执行。如果为 false,则它们将没有排序保证
* @param blockingCode 代表要运行的阻塞代码的处理程序
*/
fun <T : Any> Vertx.executeBlockingKt(ordered: Boolean = true, blockingCode: () -> T): Future<T> {
return executeBlocking(Callable {
return@Callable blockingCode()
}, ordered)
}
- 总结
尽管来说 awaitEvent
和 awaitResult
有些类似。但是实际上,他们的入参( handler ) 是不同的
// handler -> Handler<T>
val s = awaitEvent { handler ->
vertx.setTimer(1000, handler)
}
// handler -> Handler<AsyncResult<T>>)
val s2 = awaitResult { handler ->
vertx.createHttpServer().listen(8080, handler)
}
-
awaitResult
可以挂起协程等待一个 Handler<AsyncResult> 完成,并返回他的结果 -
awaitEvent
只能挂起协程等待 Handler 被执行 -
awaitEvent
可以替换awaitResult
但是这样他就不能挂起等待AsyncResult
完成并返回了
// return HttpServer 等待了 AsyncResult 的完成,并返回了结果
val s2: HttpServer = awaitResult { handler ->
vertx.createHttpServer().listen(8080, handler)
}
// return AsyncResult<HttpServer> 没有等待 AsyncResult 完成,需要开发者二次获取结果
val s3: AsyncResult<HttpServer> = awaitEvent { handler ->
vertx.createHttpServer().listen(8080, handler)
}
注意实现
- 区分
awaitResult
和awaitEvent
的使用,只需要判断是否需要等待 AsyncResult 完成 - 这三个辅助函数只能在 EventLoop 协程调度器(
vertx.dispatcher()
)中使用,如果需要在其他协程里面使用者三个辅助函数,可以使用withContext(vertx.dispatcher())
来切换协程
CoroutineScope(Dispatchers.IO).launch {
withContext(vertx.dispatcher()){
awaitBlocking {
println(456)
}
}
println("end")
}
三、CoroutineVerticle
在 Vert.x 中,Verticle 是应用程序的基本部署单元。它们是独立的组件,可以被部署到 Vert.x 实例中,并且每个 Verticle 都运行在尽可能的单独循环线程上。并且每个 Verticle 都尽量的只能被一个事件循环线程执行
CoroutineVerticle 是 Vert.x 在 Kotlin 协程支持下的一个特定实现。协程是一种轻量级的线程,允许你以同步的方式编写异步代码,而不会阻塞底层线程。这使得代码更易读和易于维护,同时保持了性能优势。
class TestCoroutineVerticle: CoroutineVerticle() {
// 允许被挂起
suspend override fun start() {
delay(1000)
// 不允许 Thrad.sleep(1000), 实现 Verticle 的也不行
// TODO
}
}
四、等待future完成
- 可以使用 CompositeFuture 来等待 future 的完成,比如
// 暂停协程直到他们完成
CompositeFuture.all(future1, future2, ...).await()
- 阻塞线程等待结果
这里封装了一个方法等待 future 完成并执行回调。通常Java 下使用这种方式,当然不是指的拓展函数,而是阻塞线程等待结果完成
/**
* 将异步 Future 封装为同步的 Promise,并等待结果返回。当完成的时候执行回调
* 该方法会阻塞线程,并等待结果返回。并在结束进行回调,如果存在回调函数
*/
fun <T> Future<T>.awaitToCompleteExceptionally(callback: ((T) -> Unit)? = null): T = CompletableFuture<T>().let {
onComplete { asyncResult ->
if (asyncResult.succeeded()) {
it.complete(asyncResult.result())
} else {
it.completeExceptionally(asyncResult.cause())
}
}
return it.get().apply {
if (callback!=null) callback(this)
}
}
- vertx 4 提供方式,挂起协程等待结果
future.coAwait()
暂停协程直到他们完成
fun <T> promise(): Promise<T> = Promise.promise<T>()
suspend fun main() {
val promise = promise<String>()
// ... (异步任务)
val future = promise.future()
// 等待任务完成并执行返回值。如果有异常则直接抛出
future.coAwait()
}
- 使用 Kotlin 本身的方法,挂起协程等待结果
封装使用 suspendCancellableCoroutine 来 暂停协程直到他们完成
/**
* 将 Vert.x 的 [Future] 对象转换为挂起函数,以在协程中使用。
*
* 该函数通过 [suspendCancellableCoroutine] 创建一个挂起函数,它允许在协程中等待异步操作完成。
*
* @return 异步操作的结果,如果操作失败,则抛出异常。
*
* @param T 异步操作的结果类型。
*/
suspend fun <T> Future<T>.await(): T = suspendCancellableCoroutine { continuation ->
onComplete { asyncResult ->
if (asyncResult.succeeded()) {
continuation.resume(asyncResult.result())
} else {
continuation.resumeWithException(asyncResult.cause())
}
}
}
- 演示一个资源场景,使用 future + 协程
代码来源
@dreamlike_ocean
情景仿照
@issues/195
代码:
// 假设这是一个连接池
val connectionPool = ConcurrentLinkedQueue<String>().apply {
addAll(listOf("conntionce-1", "connection-2", "connection-3"))
}
fun main(): Unit = runBlocking {
// 创建 Vertx 实例
val vertx = Vertx.vertx()
// 代码执行协程
// 执行 vertx.leakFun() 首先取出元素(但是获取元素会超时导致无法获取元素)
// 执行 vertx.leakFun() finally 此时会去将返回的元素放回队列中
CoroutineScope(vertx.dispatcher()).launch {
withTimeout(1000) {
vertx.leakFun()
}
}.invokeOnCompletion {
println("协程执行完毕,队列中元素数量为: ${connectionPool.size}")
vertx.setTimer(2000) {
println("两秒后 队列中的元素数量为:${connectionPool.size}")
}
}
}
suspend fun Vertx.leakFun() {
var resouce: String? = null
try {
resouce = getResource().coAwait()
} finally {
if (resouce != null) {
connectionPool.offer(resouce)
println("资源放回队列中")
}else{
println("ERR: 资源还被占用,无法将资源放回队列中")
}
println("执行 finally 方法")
}
}
fun Vertx.getResource(): Future<String> {
val promise = Promise.promise<String>()
this.setTimer(2000) {
promise.tryComplete(connectionPool.poll())
}
return promise.future()
}
在这个例子中,connectionPool
是一个 ConcurrentLinkedQueue,用来模拟一个连接池,其中包含三个连接。leakFun() 方法试图从这个连接池中取出一个资源,并且保证在任何情况下(通过 finally 块),该资源都会被放回连接池中。
问题出现在 getResource() 函数中,它设置了一个定时器,在2秒后尝试完成一个 Promise (模拟连接耗时操作),此时它会从 connectionPool 中移除一个元素(即“消耗”一个连接)。然而,withTimeout(1000) 在主函数中的调用意味着协程将等待最多1秒钟来获取资源。由于 getResource() 处理连接至少需要2s (模拟耗时比等待时间长),这就造成了超时异常,即 TimeoutCancellationException。当这种情况发生时,协程会抛出这个异常并立即取消当前的挂起操作,而不会等待 getResource() 的结果,因此连接就得不到释放。
所以,当 leakFun() 进入 finally 块时,resouce 仍然为 null,因为 getResource() 尚未完成,也就无法将资源放回队列。这就是所谓的资源泄露——资源没有被正确释放,从而可能导致以后的请求无法获取到必要的资源。
目前来说我并不知道很好的解决方法的手段,只是得知不使用超时机制
五、通道
能够使 ReadStream/WriteStream 适配为 ReceiveChannel/SendChannel 当通道满的时候会挂起当前协程
ReadStream.toReceiveChannel
老版本是toChannel
这里使用事件总线的一个监听举例子,因为他类型是 MessageConsumer extends ReadStream
val channel = vertx.eventBus().consumer<Double>("test").toReceiveChannel(vertx)
// 循环通道读取内容
for (message in channel) {
val test = message.body()
println("Received test: $test")
}
// 从通道读取一个内容,如果通道为空,则此函数将暂停,等待元素可用。如果通道是 closed for receive,则会引发异常
// channel.receive()
WriteStream.toSendChannel
val channel = serverWebSocket.toSendChannel(vertx)
// val channel2 = serverWebSocket.toReceiveChannel(vertx)
channel.send(...)
- 事件流
在 Vert.x API 的许多地方,事件流都是通过处理程序处理的。示例包括事件总线消息消费者和 HTTP 服务器请求。为了更好地整合协程和事件驱动编程模型,Vert.x 提供了ReceiveChannelHandler类,它允许通过(可挂起)receive方法接收事件。
面是一个利用ReceiveChannelHandler来处理事件流的例子:
suspend fun streamExample() {
// 创建一个 ReceiveChannelHandler 来适配事件总线的消息消费者
val adapter = vertx.receiveChannelHandler<Message<Int>>()
// 将适配器设置为本地消费者的处理器
vertx.eventBus().localConsumer<Int>("a.b.c").handler(adapter)
// 发送 15 条消息到事件总线
for (i in 0 until 15) { // 注意这里的范围应该是 until 而不是 .. 因为后者包含上限值
vertx.eventBus().send("a.b.c", i)
}
// 接收前 10 条消息
for (i in 0 until 10) {
val message = adapter.receive()
println("Received: ${message.body()}")
}
}
在这个例子中,我们创建了一个ReceiveChannelHandler实例并将其作为处理器注册给事件总线上的本地消费者。然后,我们发送了一系列整数消息到事件总线,并使用adapter.receive()方法以挂起的方式接收这些消息。当没有更多消息可接收时,receive调用将会暂停协程,直到有新的消息到达或通道被关闭。
- 事件流还可以在什么地方使用?
事件流在Vert.x中是核心概念之一,它允许以非阻塞的方式处理I/O和其它异步操作。通过使用事件流,可以创建高度可扩展的应用程序,这些应用程序能够处理大量的并发连接而不会造成线程资源的浪费。除了已经提到的事件总线消息消费者和HTTP服务器请求外还有,文件读取与写入、网络套接字通信、数据库交互、定时任务。
还可以将多个不同的事件流合并为一个单一的流,以便同时监听多种事件源的变化
总之,在WriteStream/ReadStream 中基本都可以使用 vertx.receiveChannelHandler
六、Mutex
通过 Mutex 可以防止多个协程同时操作与访问一个资源,这同样对EventLoop 开启的协程有效
fun main() = runBlocking {
val vertx = Vertx.vertx()
// 创建一个Mutex用于同步
val mutex = Mutex()
// 共享资源:计数器
var counter = 0
// 定义一个挂起函数来更新计数器
suspend fun updateCounter(value: Int) {
withContext(vertx.dispatcher()) { // 确保在Vert.x事件循环线程上执行
mutex.withLock {
counter++
println("Counter updated to $counter by ${Thread.currentThread().name}")
}
}
}
// 模拟并发更新计数器
repeat(100) { i ->
launch {
delay((1..50).random().toLong()) // 模拟异步任务
updateCounter(i)
}
}
// 等待所有协程完成
delay(2000) // 给足够的时间让所有的发送都完成
println("Final counter value: $counter")
}
七、分享一个好玩的邪道
通过反射获取 vertx 的内部的 worker 线程池,并转为一个 CoroutineDispatcher 。
/**
* 获取一个与 Vertx 工作线程对应的协程调度器。
* 该调度器是复用了 Vertx 工作线程池,因此可能会导致意外的调度错误
*/
fun Dispatchers.vertxWorker(vertx: Vertx = GLOBAL_VERTX_INSTANCE): CoroutineDispatcher {
var context = Vertx.currentContext() ?: vertx.orCreateContext
val worker = context.get<ExecutorCoroutineDispatcher>("worker_ExecutorCoroutineDispatcher") ?: run {
SystemLogger.warn("你正在使用 Vertx 工作线程池转化的协程调度器,该调度器可能存在调度BUG,执行 Vertx 代码时可能导致上下文不匹配问题")
val workerPoolField = try {
if (context is ContextImpl) {
return@run (context as ContextImpl).workerPool().executor().asCoroutineDispatcher()
}
context::class.java.getDeclaredField("workerPool").apply { isAccessible = true }
} catch (e: NoSuchFieldException) {
val delegateField = context::class.java.getDeclaredField("delegate").apply { isAccessible = true }
context = delegateField.get(context) as Context
context::class.java.getDeclaredField("workerPool").apply { isAccessible = true }
}
(workerPoolField.get(context) as WorkerPool).executor().asCoroutineDispatcher()
}
context.put("worker_ExecutorCoroutineDispatcher", worker)
return worker
}
使用方式
CoroutineScope(Dispatchers.vertxWorker(GLOBAL_VERTX_INSTANCE)).launch {
// 开启一个在 Vertx 的工作线程池上的一个协程
}
图一乐罢了,不建议使用,当然我还是使用的,毕竟有点小意思嘿嘿
八、判断 promise
是否存在监听
/**
*
* @author : zimo
* @date : 2023/12/21
*/
fun <T> promise(): Promise<T> = Promise.promise<T>()
fun <T> Promise<T>.isCompleted(): Boolean = this.future().isComplete
fun <T> Promise<T>.isSucceeded(): Boolean = this.future().succeeded()
fun <T> Promise<T>.isFailed(): Boolean = this.future().failed()
/**
* 是否有监听器存在。
*/
fun <T> Promise<T>.isNotListener(): Boolean {
val clazz2 = this::class.java
val superclass = clazz2.superclass
val field1 = superclass.getDeclaredField("listener") // 监听器,如果未有监听或者监听已经触发完毕了
field1.isAccessible = true
val listener = field1.get(this)
return listener == null
}
/**
* 是否是初始状态
* Promise 没有 complete,并且没有监听器
*/
fun <T> Promise<T>.isInitialStage(): Boolean = !isCompleted() && isNotListener()
九、promise/future
Vert.x 使用 Future 来表示异步计算的结果,而 Promise 则是创建 Future 的一种方式,并且它允许你完成(成功或失败)这个 Future
- Promise:是一个可以用来完成 Future 的对象。你可以调用 promise.complete(result) 或 promise.fail(cause) 来设置 Future 的结果。
- Future:代表一个异步操作的结果。它可以被监听以获取操作的成功或失败状态。
fun createPromise(): Promise<String> {
val promise = Promise.promise<String>()
// 执行一些异步操作...
// 最终通过 promise.complete() 或 promise.fail() 来完成
return promise
}
// 获取 Future
val future: Future<String> = createPromise()
十、对比 Java 和 Kotlin 写法
Java:
public static void main(String[] args) {
// 创建 Vertx 实例
Vertx vertx = Vertx.vertx();
// 目标文件路径
String targetFilePath = "./test.txt";
// 异步读取源文件内容
vertx.fileSystem().readFile(targetFilePath, readFileResult -> {
if (readFileResult.succeeded()) {
// 成功读取文件
Buffer fileContent = readFileResult.result();
// 复制内容两次
Buffer doubledContent = fileContent.copy().appendBuffer(fileContent);
// 异步写入目标文件
vertx.fileSystem().writeFile(targetFilePath, doubledContent, writeFileResult -> {
if (writeFileResult.succeeded()) {
System.out.println("成功写入文件: " + targetFilePath);
} else {
System.err.println("写入文件出错: " + writeFileResult.cause());
}
});
} else {
// 读取文件失败
System.err.println("读取文件出错: " + readFileResult.cause());
}
});
}
Kotlin:
fun main(): Unit = runBlocking {
// 创建 Vertx 实例
val vertx = Vertx.vertx()
// 目标文件路径
val targetFilePath = "./test.txt"
// 使用协程作用域
CoroutineScope(vertx.dispatcher()).launch {
// 异步读取源文件内容并等待结果
val fileContent = awaitResult<io.vertx.core.buffer.Buffer> {
vertx.fileSystem().readFile(targetFilePath, it)
}
// 复制内容两次
val doubledContent = fileContent.copy().appendBuffer(fileContent)
// 异步写入目标文件并等待结果
awaitResult<Void> {
vertx.fileSystem().writeFile(targetFilePath, doubledContent, it)
}
println("成功写入文件: $targetFilePath")
}
}
fun main2(): Unit = runBlocking {
// 创建 Vertx 实例
val vertx = Vertx.vertx()
// 目标文件路径
val targetFilePath = "./test.txt"
// 使用协程作用域
CoroutineScope(vertx.dispatcher()).launch {
// 异步读取源文件内容并等待结果
val fileContent = vertx.fileSystem().readFile(targetFilePath).coAwait()
// 复制内容两次
val doubledContent = fileContent.copy().appendBuffer(fileContent)
// 异步写入目标文件并等待结果
vertx.fileSystem().writeFile(targetFilePath, doubledContent).coAwait()
println("成功写入文件: $targetFilePath")
}
}
使用协程可以将异步调用写成同步风格的代码,使得代码看起来更加线性和直观。这与使用回调不同,后者会导致嵌套的匿名函数或lambda表达式,随着异步操作数量的增加,代码会变得难以阅读和维护。