Vertx 使用 Kotlin 的协程

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

一、开启协程

  1. 获取 Vertx 提供的事件循环协程调度器
val vertxDispatcher = vertx.dispatcher()
  1. 使用调度器创建协程,该协程调度器和 Dispatchers.IO 等调度器使用方法一致

需要注意的是,该协程使用是 EventLoop 生成的调度器,所以同样和使用 EventLoop 一样不能使用阻塞代码

 CoroutineScope(vertx.dispatcher()).launch {
     delay(1000)
 }
  1. Vert.x 还提供了一个返回io.vertx.core.Future实例的协程构建器。
val future1: Future<String> = vertxFuture(vertx) {
    //...
    "Result"
}
  1. 定义子协程
 CoroutineScope(vertx.dispatcher()).launch {
     delay(1000)
     coroutineScope{
		 delay(1000)
     }
 }
注意事项
  • 避免阻塞代码:正如上面提到的,当使用 vertxDispatcher 创建的协程时,必须避免任何可能会阻塞事件循环线程的操作。如果你确实需要执行阻塞操作,考虑使用 awaitBlocking 或者切换到另一个适合执行阻塞任务的调度器,例如 Dispatchers.IO。
  • 资源管理:确保正确管理协程作用域的生命周期。比如,在适当的时候取消 Coroutine,以防止潜在的内存泄漏或不必要的后台工作。
  • 禁止在 Vert.x 事件循环线程中使用runBlocking。也同样在Vertx 的协程中禁止使用 runBlocking
  • 为了避免内存泄漏,请始终使用coroutineScope {..}定义子范围。这样,如果范围内的协程失败,则在该范围内定义的所有其他协程也将被取消。

二、辅助函数

awaitEvent, awaitResult, 和 awaitBlocking 是用于简化异步编程的辅助函数

  1. awaitEvent 等待一个事件发生

异步执行阻塞代码,直到阻塞到事件发生
执行该代码后会挂起当前协程,并等待 awaitEvent 执行完毕

val s = awaitEvent { handler ->   
// 协程会被阻塞,直到事件发生,这个动作不会阻塞 vertx 的事件循环。
// 只要将 awaitEvent 的入参 handler 执行即可
	server.setTimer(1000, handler)
}
  1. awaitResult 等待结果返回

异步运行阻塞并等待结果。
使用 awaitResult 可以等待具有Handler<AsyncResult> 返回值的方法代码执行完毕,并最后调用handler 恢复挂起的当前协程 。通常是将handler 作为参数传入,如代码所示

  • onCompletion awaitResult 返回完成 future 的值
  • onFailure awaitResult 抛出 future 失败的异常
// 这个处理程序可以传递给 Vert. x 异步方法
// 协程将被阻塞,直到 future 完成或失败,此操作不会阻塞 vertx 的事件循环。
val s = awaitResult { handler ->  
 server.listen(8080, handler) 
}
  1. 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)
}
  1. 总结

尽管来说 awaitEventawaitResult 有些类似。但是实际上,他们的入参( 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)
}
  1. awaitResult 可以挂起协程等待一个 Handler<AsyncResult> 完成,并返回他的结果

  2. awaitEvent 只能挂起协程等待 Handler 被执行

  3. 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)
 }
注意实现
  1. 区分awaitResultawaitEvent 的使用,只需要判断是否需要等待 AsyncResult 完成
  2. 这三个辅助函数只能在 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完成

  1. 可以使用 CompositeFuture 来等待 future 的完成,比如
// 暂停协程直到他们完成
CompositeFuture.all(future1, future2, ...).await()
  1. 阻塞线程等待结果

这里封装了一个方法等待 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)
    }
}
  1. 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()
}
  1. 使用 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())
        }
    }
}

  1. 演示一个资源场景,使用 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 当通道满的时候会挂起当前协程

  1. 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()
  1. WriteStream.toSendChannel
val channel = serverWebSocket.toSendChannel(vertx)
// val channel2 = serverWebSocket.toReceiveChannel(vertx)
channel.send(...)
  1. 事件流

在 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调用将会暂停协程,直到有新的消息到达或通道被关闭。

  1. 事件流还可以在什么地方使用?

事件流在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表达式,随着异步操作数量的增加,代码会变得难以阅读和维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值