有没有 vert.x 或者有关注 Java 虚拟线程的群

2023-05-03 09:45:57 +08:00
 byte10

一、背景

最近使用 vert.x 在开发一个 java 程序,只所以用 vert.x 就是因为它比较适合开发这类的程序。但是异步编程开发起来实在不好维护。。实现也太麻烦了,实在是太复杂了。下单完还要判断是否成功,还有循环下单等,很多时候异步实现起来非常的不好实现和阅读。

二、引入虚拟线程

目前引入的虚拟线程遇到非常大的麻烦。一般一个 verticle 绑定一个 eventloop 线程,我把虚拟线程绑定 vert.x eventloop 的线程中,,这样每个 verticle 内使用的所有虚拟线程也是它们自己平台 eventloop 线程,也就是在 verticle 使用的所有虚拟线程和自己平台线程 都是同一个线程,所以理论也是线程安全的。目前遇到的是虚拟线程和它自己的平台线程在执行 log 日志输出的时候,就会遇到死锁。也就是虚拟线程和它自己的平台线程发生了竞争 log.info 的输出。

下面是异常日志:

Thread Thread[#63,vert.x-eventloop-thread-1,5,main] has been blocked for 10586 ms, time limit is 2000 ms
io.vertx.core.VertxException: Thread blocked
        at java.base/jdk.internal.misc.Unsafe.park(Native Method)
        at java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:221)
        at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:754)
        at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:990)
        at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lock(ReentrantLock.java:153)
        at java.base/java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:322)
        at ch.qos.logback.core.OutputStreamAppender.writeBytes(OutputStreamAppender.java:200)
        at ch.qos.logback.core.OutputStreamAppender.writeOut(OutputStreamAppender.java:193)
        at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:228)
        at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:102)
        at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:85)
        at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
        at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:272)
        at ch.qos.logback.classic.Logger.callAppenders(Logger.java:259)
        at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:426)

使用 jstack -l pid 检查确实一直 block 中.

LockSupport.class

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        try {
            if (t.isVirtual()) {
                VirtualThreads.park();
            } else {
                U.park(false, 0L);
            }
        } finally {
            setBlocker(t, null);
        }
    }

由于 verticle 可能是很多个,可能会绑定到同一个 eventloop 线程,所以很难从代码上比较难规避不使用 eventloop 平台线程 。

一般使用 ReentrantLock.lock 产生死锁的情况是什么原因呢?个人感觉跟 vert.x 关系不大,应该跟虚拟线程的有关系。

目前不确定是否 ReentrantLock.lock 的问题,还是 logback 日志框架的问题。尝试直接用 ReentrantLock.lock 进行测试,额-也没有复现。。。但是用 log.info 打印日志必现。

3336 次点击
所在节点    Java
30 条回复
lixintcwdsg
2023-05-03 10:15:56 +08:00
虚拟线程之前也深入看了一下,说个观点看对你有没有帮助:
虚拟线程就是希望干掉 eventloop 的,或者说,所有 netty 衍生类框架,网络 IO (收发包,HTTP 编解码等)依然用线程池+eventloop 可以,然后业务线程池直接换成虚拟线程来实现。比如 netty 之前强调的业务线程池需要独立的问题。
byte10
2023-05-03 10:52:38 +08:00
@lixintcwdsg 额,这个和 vertx 不太一样。你说业务线程池是可以直接用虚拟线程,但是业务线程会产生线程并发的安全问题,所以 verticle 才需要绑定一个 eventloop 线程。而我的虚拟线程 都是从在这个 eventloop 线程上运行,来确保我所有的业务都是线程安全的。

现在的问题 平台 eventloop 线程直接去运行 log.info() 这个方法就会出问题,我无法控制这个 eventloop 线程 会不会被其他的地方使用到,而且它是 vert.x 框架的东西,里面会不会有其他的坑我也不知道。我可以确保 我的代码不会直接使用 eventloop 线程 执行任何 代码,但是也不能确保其他人开发 会不会使用上。而且日志是刚需。。。比较难搞。

使用虚拟线程 本质是为了 解决异步的问题,如果用普通线程池也可以解决,但是就会产生多线程安全的问题。如果我的虚拟线程 固定在定一个 eventloop 线程运行,就能保证线程并发安全,又能解决异步的问题。
lightjiao
2023-05-03 10:52:55 +08:00
Java 没有 async await 么?异步编程我现在只推这个模型,贼舒服
dreamlike
2023-05-03 10:55:03 +08:00
你遇到了一个 quarkus 之前遇到的问题 答案是不要用 eventloop 作为虚拟线程调度器
或者通过一些手段 使用 continuation api 来做 await
dreamlike
2023-05-03 11:02:43 +08:00
loom 当前的情况似乎并不适合用 vertx ,kt corotinue 会更适合一些
只有当 loom 解决了 sychronized 等问题 或者 旧生态中逐渐兼容了这些问题 在 vertx 中使用 loom 才有一些优势
目前使用 virtual thread 相比 kt corotinue 的优势只有适合保存完整堆栈信息
byte10
2023-05-03 11:26:57 +08:00
@lightjiao 额,java 确实是没有 async 和 await 。vert.x 确实很强,设计上就解决了很多问题。
@dreamlike continuation api ,我去研究下。但是这个 continuation 能否解决 多线程安全的问题呢 ,因为我的目的是避免多线程安全问题。
dreamlike
2023-05-03 11:58:29 +08:00
@byte10 continuaion 有俩原语,yield 将控制权返回给调用方,resume 从上一次 yield 的位置继续执行,java 基于这个玩意实现的有栈协程,单独拿出来用就是自己搞调度,可以参考我写的这个库的实现 https://github.com/dreamlike-ocean/UnsafeVirtualThread/blob/master/vertx-ext/src/main/java/top/dreamlike/AsyncScope.java
lixintcwdsg
2023-05-03 12:59:16 +08:00
@byte10 eventloop 用虚拟线程的不太理解这个场景,eventloop+tasklist 本来是虚拟线程或者说协程的平替,为何在 event 的模式下用虚拟线程这个很奇怪,虚拟线程都需要大量创建了,还去 loop tasklist 干啥呢。。。
WispZhan
2023-05-03 14:23:15 +08:00
@lixintcwdsg +1
@dreamlike 说得对

VirtualThreads 虚线程的设计其实,和 EventLoop 或者 Kt Coroutine 不在一个层次。它是给底层库或者框架开发者使用的。
所以没必要和 EventLoop 强行从用户侧适配,如果想更优雅实现,直接用现成的 vertex-kotlin-coroutine 不是更省事么
不然就直接用 VirtualThreads 去改 Vertx 底层实现算了。

建议看看这个:
<amp-youtube data-videoid="1qezCNVWpHc" layout="responsive" width="480" height="270"></amp-youtube>
以及在前不久的 KotlinConf2023 上 Roman Elizarov 的演讲
https://kotlinconf.com/speakers/80f570c3-27df-4756-b04a-76b2d6f220c4/#Coroutines%20and%20Loom%20behind%20the%20scenes
这个找不到视频了,有兴趣可以看看
WispZhan
2023-05-03 14:33:55 +08:00
没写完就发了。 简而言之 VirtualThreads 和 目前各种 JVM 的 Coroutine 库之间,设计目标不一样,解决的问题也不一样。

---

另外需要吐槽的是,写 Reactive 的代码为啥这么多锁和 Blocked 逻辑,Vert.X 的"The Golden Rule - Don’t Block the Event Loop"
有没有可能你写的场景,并不适合它。
byte10
2023-05-03 15:59:21 +08:00
@lixintcwdsg 我觉得 虚拟线程并不是干掉 eventloop 的。vert.x actor 模型下 多线程并发变得安全,虚拟线程并没有解决线程安全的问题。即便有虚拟线程, 仍然需要它 来解决多线程并发的问题。
byte10
2023-05-03 16:21:16 +08:00
@lixintcwdsg @lightjiao @WispZhan @dreamlike

这个一个代码示例,一个是异步写法,一个是同步的写法。我是用虚拟线程就是了能解决异步编程的问题。当然是用传统的线程池也可以解决异步编程的问题,但是明显太重。。 这个就是我是用虚拟线程的原因。


```
public class AsyncVerticle extends AbstractVerticle {

private Map<String, String> map = new HashMap<>(2);

public void start() throws Exception {
// map 的操作是线程安全的,但是 http 请求代码的模式是异步编程
vertx.setPeriodic(123, id -> {
httpRequest().onSuccess(res -> {
if (res != null) {
httpRequest().onSuccess(nextRes -> {
map.put("result", nextRes);
});
}
});
});
vertx.eventBus().consumer("httpReq", event -> {
httpRequest().onSuccess(res -> {
map.put("result", res);
});
});

// =====================使用虚拟线程,并且使用同步编程==================
SyncVertxWraper syncVertx = new SyncVertxWraper((VertxInternal) getVertx(), true);
// 启动虚拟线程运行,下面代码全是运行在虚拟线程中
syncVertx.getOrCreateContext().runOnContext((ctx) -> {
vertx.setPeriodic(123, id -> {
// 运行在虚拟线程中,如果虚拟线程绑定在同一个 eventloop 线程中,那么 map 就会线程安全。异步可以转成同步写法
String res = Async.await( httpRequest());
if (res != null) {
res = Async.await( httpRequest());
map.put("result", res);
}
});
vertx.eventBus().consumer("httpReq", httpResult -> {
// 运行在虚拟线程中,如果虚拟线程绑定在同一个 eventloop 线程中,那么 map 就会线程安全。
String res = Async.await( httpRequest());
map.put("result", res);
});
});
}

}
```
byte10
2023-05-03 16:31:43 +08:00
@lixintcwdsg @lightjiao @WispZhan @dreamlike

我添加代码到 附言那里,刚才那个代码展示太乱了,不方便阅读。那代码示例 应该很清晰描述我的应用场景了。

目前还不想使用 kotlin 来重构,太多东西了。
byte10
2023-05-03 16:42:48 +08:00
@dreamlike
@WispZhan
两位提到的 continuaion Coroutine 确实是我的想要的东西 应该能解决异步编程问题,不知道实现起来是否很难。。回头研究下,那就可以不用虚拟线程了。
dreamlike
2023-05-03 17:21:16 +08:00
@byte10 我提到的 continuation api 是一个内部 api 我也是通过一个非常“邪恶”的方法来拿出来用的 如果不在乎升级的阻力 这个可以直接用我的方案。。。
否则还是 kt 那个靠谱
goofyluo2023
2023-05-03 17:23:36 +08:00
Java 里用协程带来复杂性,性能也有所降低,建议简单逻辑用 future 解决,复杂逻辑用 rxjava
leatomic
2023-05-03 17:34:28 +08:00
@dreamlike 虚拟线程核心就是提供一个替代线程的模型吧(线程是“通用”操作系统的任务承载体,这个调度机制要支持很多特性包括一些统计,而这其中大部分都是不需要的就是太重了,除了带来不必要的元数据占用内存空间,还有这些数据的保存变更逻辑就是上下文切换,而且涉及影响其他进程还需要陷入内核模式执行),线程池解决占多余内存的问题,阻塞会导致并行度丢失于是需要引入并行度补偿机制,但在阻塞密集的时候又创建了太多 worker thread 又趋近于 thread per task 了,解决办法就是(在需要进行上下文切换的地方,就是阻塞 /等待,内在逻辑也是注册回调条件成熟唤醒)用更加轻量级的上下文切换替代线程的,就是虚拟线程的,或者额外搞一种支持注册回调并会 poll 唤醒的机制直接摊牌不阻塞了拆成有依赖关系的多个分支任务(上下文丢失除非又保存,当然可以做得比较灵活在调试时才开启,真实性能差距不得而知)。
JDK 中应该只会在确定当前任务的承载体是虚拟线程时在切换的时候才会进行虚拟线程的上下文切换(而不是线程的上下文切换)。问题来了,kt corotinue 中的代码,能有阻塞(真正意义上的,能调试的那种)的逻辑吗,如果有,发生的上下文切换,还是线程的上下文切换吧。
leatomic
2023-05-03 17:53:03 +08:00
@dreamlike 上面第二段的切换值得的是阻塞的时候(目前是只包括基于 JUC 的同步器的以及网络 I/O 的吧)。类似 kt corotinue 等无栈(例如基于方法参数传递状态机的?)能不能做到这种 Hook 我也不知道,我想表达的是,JDK 应该不会支持,毕竟每次 resume 就跟新的一次方法调用一样调用链都丢了,调试困难。
leatomic
2023-05-03 18:07:34 +08:00
@lixintcwdsg 内存占用方面(毕竟阻塞 /等待,注册事件回调后方法就返回了,响应的调用栈(的栈帧链)就回收了,回调时再重新算)还是明显 eventloop 这种香的,就是不好调试,然而对于一些经过长时间运行测试,很稳定的逻辑,例如 Netty 自身的 I/O 处理的,没事,本身更多关注业务逻辑,调试也只会调试业务逻辑的(问题基本都处在这),所以 Netty 的线程模型应该不会变,上下文切换这块不得而知。
而处理业务逻辑部分,开启虚拟线程的边界,用虚拟线程 carry 更合适,不冲突的,与其说平替,不如说互补吧
byte10
2023-05-03 19:15:52 +08:00
@dreamlike 嗯好的,你那个不错,我研究下。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/936958

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX