最近使用 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 打印日志必现。
关于为何要实现虚拟线程,我这里列举一下代码。普通开发模式就是异步编程,一直嵌套。如果引入虚拟线程就可以解决异步编程的问题,但是遇到我上面所描述的问题。因为eventloop线程在别的地方被使用了,或者在别的Verticle被绑定了使用。
vert.x 的虚拟线程 incubator项目,我的代码实现大部分来之这里。
https://github.com/vert-x3/vertx-virtual-threads-incubator
public class AsyncVerticle extends AbstractVerticle {
private Map<String, String> map = new HashMap<>(2);
@Override
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 -> {
// 运行在虚拟线程中,异步可以转成同步写法
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);
});
});
}
}
1
lixintcwdsg 2023-05-03 10:15:56 +08:00 1
虚拟线程之前也深入看了一下,说个观点看对你有没有帮助:
虚拟线程就是希望干掉 eventloop 的,或者说,所有 netty 衍生类框架,网络 IO (收发包,HTTP 编解码等)依然用线程池+eventloop 可以,然后业务线程池直接换成虚拟线程来实现。比如 netty 之前强调的业务线程池需要独立的问题。 |
2
byte10 OP @lixintcwdsg 额,这个和 vertx 不太一样。你说业务线程池是可以直接用虚拟线程,但是业务线程会产生线程并发的安全问题,所以 verticle 才需要绑定一个 eventloop 线程。而我的虚拟线程 都是从在这个 eventloop 线程上运行,来确保我所有的业务都是线程安全的。
现在的问题 平台 eventloop 线程直接去运行 log.info() 这个方法就会出问题,我无法控制这个 eventloop 线程 会不会被其他的地方使用到,而且它是 vert.x 框架的东西,里面会不会有其他的坑我也不知道。我可以确保 我的代码不会直接使用 eventloop 线程 执行任何 代码,但是也不能确保其他人开发 会不会使用上。而且日志是刚需。。。比较难搞。 使用虚拟线程 本质是为了 解决异步的问题,如果用普通线程池也可以解决,但是就会产生多线程安全的问题。如果我的虚拟线程 固定在定一个 eventloop 线程运行,就能保证线程并发安全,又能解决异步的问题。 |
3
lightjiao 2023-05-03 10:52:55 +08:00 via iPhone
Java 没有 async await 么?异步编程我现在只推这个模型,贼舒服
|
4
dreamlike 2023-05-03 10:55:03 +08:00 via Android
你遇到了一个 quarkus 之前遇到的问题 答案是不要用 eventloop 作为虚拟线程调度器
或者通过一些手段 使用 continuation api 来做 await |
5
dreamlike 2023-05-03 11:02:43 +08:00 via Android 1
loom 当前的情况似乎并不适合用 vertx ,kt corotinue 会更适合一些
只有当 loom 解决了 sychronized 等问题 或者 旧生态中逐渐兼容了这些问题 在 vertx 中使用 loom 才有一些优势 目前使用 virtual thread 相比 kt corotinue 的优势只有适合保存完整堆栈信息 |
6
byte10 OP |
7
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
|
8
lixintcwdsg 2023-05-03 12:59:16 +08:00 1
@byte10 eventloop 用虚拟线程的不太理解这个场景,eventloop+tasklist 本来是虚拟线程或者说协程的平替,为何在 event 的模式下用虚拟线程这个很奇怪,虚拟线程都需要大量创建了,还去 loop tasklist 干啥呢。。。
|
9
WispZhan 2023-05-03 14:23:15 +08:00
@lixintcwdsg +1
@dreamlike 说得对 VirtualThreads 虚线程的设计其实,和 EventLoop 或者 Kt Coroutine 不在一个层次。它是给底层库或者框架开发者使用的。 所以没必要和 EventLoop 强行从用户侧适配,如果想更优雅实现,直接用现成的 vertex-kotlin-coroutine 不是更省事么 不然就直接用 VirtualThreads 去改 Vertx 底层实现算了。 建议看看这个: 以及在前不久的 KotlinConf2023 上 Roman Elizarov 的演讲 https://kotlinconf.com/speakers/80f570c3-27df-4756-b04a-76b2d6f220c4/#Coroutines%20and%20Loom%20behind%20the%20scenes 这个找不到视频了,有兴趣可以看看 |
10
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" 有没有可能你写的场景,并不适合它。 |
11
byte10 OP @lixintcwdsg 我觉得 虚拟线程并不是干掉 eventloop 的。vert.x actor 模型下 多线程并发变得安全,虚拟线程并没有解决线程安全的问题。即便有虚拟线程, 仍然需要它 来解决多线程并发的问题。
|
12
byte10 OP @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); }); }); } } ``` |
13
byte10 OP @lixintcwdsg @lightjiao @WispZhan @dreamlike
我添加代码到 附言那里,刚才那个代码展示太乱了,不方便阅读。那代码示例 应该很清晰描述我的应用场景了。 目前还不想使用 kotlin 来重构,太多东西了。 |
14
byte10 OP |
15
dreamlike 2023-05-03 17:21:16 +08:00 via Android
@byte10 我提到的 continuation api 是一个内部 api 我也是通过一个非常“邪恶”的方法来拿出来用的 如果不在乎升级的阻力 这个可以直接用我的方案。。。
否则还是 kt 那个靠谱 |
16
goofyluo2023 2023-05-03 17:23:36 +08:00
Java 里用协程带来复杂性,性能也有所降低,建议简单逻辑用 future 解决,复杂逻辑用 rxjava
|
17
leatomic 2023-05-03 17:34:28 +08:00
@dreamlike 虚拟线程核心就是提供一个替代线程的模型吧(线程是“通用”操作系统的任务承载体,这个调度机制要支持很多特性包括一些统计,而这其中大部分都是不需要的就是太重了,除了带来不必要的元数据占用内存空间,还有这些数据的保存变更逻辑就是上下文切换,而且涉及影响其他进程还需要陷入内核模式执行),线程池解决占多余内存的问题,阻塞会导致并行度丢失于是需要引入并行度补偿机制,但在阻塞密集的时候又创建了太多 worker thread 又趋近于 thread per task 了,解决办法就是(在需要进行上下文切换的地方,就是阻塞 /等待,内在逻辑也是注册回调条件成熟唤醒)用更加轻量级的上下文切换替代线程的,就是虚拟线程的,或者额外搞一种支持注册回调并会 poll 唤醒的机制直接摊牌不阻塞了拆成有依赖关系的多个分支任务(上下文丢失除非又保存,当然可以做得比较灵活在调试时才开启,真实性能差距不得而知)。
JDK 中应该只会在确定当前任务的承载体是虚拟线程时在切换的时候才会进行虚拟线程的上下文切换(而不是线程的上下文切换)。问题来了,kt corotinue 中的代码,能有阻塞(真正意义上的,能调试的那种)的逻辑吗,如果有,发生的上下文切换,还是线程的上下文切换吧。 |
18
leatomic 2023-05-03 17:53:03 +08:00
@dreamlike 上面第二段的切换值得的是阻塞的时候(目前是只包括基于 JUC 的同步器的以及网络 I/O 的吧)。类似 kt corotinue 等无栈(例如基于方法参数传递状态机的?)能不能做到这种 Hook 我也不知道,我想表达的是,JDK 应该不会支持,毕竟每次 resume 就跟新的一次方法调用一样调用链都丢了,调试困难。
|
19
leatomic 2023-05-03 18:07:34 +08:00
@lixintcwdsg 内存占用方面(毕竟阻塞 /等待,注册事件回调后方法就返回了,响应的调用栈(的栈帧链)就回收了,回调时再重新算)还是明显 eventloop 这种香的,就是不好调试,然而对于一些经过长时间运行测试,很稳定的逻辑,例如 Netty 自身的 I/O 处理的,没事,本身更多关注业务逻辑,调试也只会调试业务逻辑的(问题基本都处在这),所以 Netty 的线程模型应该不会变,上下文切换这块不得而知。
而处理业务逻辑部分,开启虚拟线程的边界,用虚拟线程 carry 更合适,不冲突的,与其说平替,不如说互补吧 |
21
byte10 OP @leatomic 好家伙这个调试 也是大问题,看来还是要虚拟线程才行,,不然一直堵塞 eventloop 的 代码。。还是引入虚拟线程比较好调试。
|
22
yazinnnn 2023-05-03 19:30:04 +08:00
1. 如果你在 vertx 里用锁去同步代码, 很可能你的 vertx 使用方式有问题
2. 如果是回调嵌套太深的话, 用 future/promise 去打平代码逻辑,或者用 kotlin 协程去打平, 回调 api 在 vertx5 中会废弃 3. vertx 这种基于 netty 的 nio 框架还是更适合 kotlin 协程, loom 更适合传统 bio 框架 4. 如果你的业务稍微有些复杂度的话,建议上 quarkus |
23
D3EP 2023-05-03 19:32:09 +08:00
这日志不是死锁,而是 EventLoop 因为日志打印被阻塞住了。Logback 配置有缺陷,同步刷盘肯定会影响 EventLoop ,改成 AsyncAppender 就好了。
|
24
lixintcwdsg 2023-05-03 20:27:54 +08:00
@leatomic 我给楼主建议的就是互补,业务逻辑用虚拟线程,楼主硬要融合嘛不是,这本就是一件事的两个解决方案。
|
25
lixintcwdsg 2023-05-03 20:36:28 +08:00
@byte10
大家对 eventloop 的理解是不是有误会。 netty 类型 eventloop 的线程安全是针对单个连接的,实际上除了游戏和 IM 服务器一根 TCP 长连接直连 netty-base 的服务器(其实这比较罕见,因为没经过网关),其余情况下要么是 eventloop 单线程跑,要不根本谈不上线程安全。 对于 http 短链接来说,你用 eventloop 下的某一个 loop 的线程跑和用虚拟线程跑业务都没有线程安全问题,一次连接一个线程。 虚拟线程配合 eventloop ,虚拟线程处理结束要回写 task 给当前 loop 线程的 task 队列。 另,如果要讲究线程安全,数据使用的方式就不要用共享内存的形式。 另,没记错虚拟线程的 JEP 里面明确有讲,不推荐虚拟线程共享变量。 |
26
byte10 OP |
27
byte10 OP @lixintcwdsg 嗯 感谢,对于 eventloop 我可能没你理解那么深刻。vert.x 的 Verticle 就是线程安全的,因为它绑定在同一 eventloop 线程中,任何时刻的代码都运行在同一个线程,所以不存在并发安全。我代码贴出的代码示例,有描述我的需求,就是想把异步编程转成同步编程。虚拟线程可以做到。如果使用虚拟线程,那么就会存在线程安全问题,所以我才想把它绑定到 eventloop 的 EventLoop carrier ,这样就可以绑定到同一个 EventLoop 线程了,就不存在线程安全问题了
|
28
kaneg 2023-05-04 00:14:57 +08:00
虚拟线程的一个重要影响是取代线程池:因为其创建代价极小,所以意在减少线程创建开销而生的线程池就没有存在的意义了。
至于多线程本身的编程模型,虚拟线程应该还是遵守的。 |
29
asssfsdfw 2023-05-04 16:11:45 +08:00
deadlock 两则,关于 virtual thread 的。
https://github.com/netty/netty/issues/13204 https://jira.qos.ch/projects/LOGBACK/issues/LOGBACK-1711?filter=allopenissues |