The attempt to transplant jtreg test GetStackTraceWhenRunnable.java from Loom has failed #128
sunrise-cn
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
GetStackTraceWhenRunnable.java移植失败情况说明
测试概要
移植任务介绍:
将loom的GetStackTraceWhenRunnable.java移植到TencentKonaFiber-8的相应位置下观察KonaFiber与Loom是否能产生一致行为,若不能则分析原因。
GetStackTraceWhenRunnable.java的测试目标:
确认当系统资只剩下一个
platform thread
时,且该platform thread
已经被某一个virtual thread
占用,其它处于runnable
状态的virtual thread
是否可以正常打印出调用栈信息。测试代码分析
为方便叙述问题,相比原测试代码,我增加了
thread1
和thread2
的名字:thread1.setName("virtual_thread1");
thread2.setName("virtual_thread2");
代码介绍:
环境准备:使用
-Djdk.virtualThreadScheduler.maxPoolSize=1
参数保证了ForkJoinPool
里只有1个platform thread
测试代码任务:设定只有1个
platform thread
,通过一系列操做使得virtual thread1
处于RUNNABLE
状态,此时尝试它是否能够正常打印栈信息。测试代码逐行分析:
创建
virtual_thread1
:调用LockSupport#park()
使其进入WAITING
状态,这一步virtual_thread1
从platform thread
unmount
下来。通过多路复用器
selector
,使virtual_thread2
竞争上platform thread
之后一直pin
到该platform thread
通过
CoundDownLatch
让main thread
等待,直到virtual_thread2
占据到platform thread
main thread
获得执行权后,继续向下执行。调用
LockSupport#unpark()
使virtual_thread1
从WAITING
转变为RUNNABLE
状态。virtual_thread1
一直尝试重新竞争上platform thread
但始终竞争不上的状态。virtual_thread1
处于RUNNABLE
状态。通过
for
循环连续判断5次virthread_thread1
是否保持在RUNNABLE
状态。virtual_thread1
调用getStackTrace()
方法尝试获取栈信息。RuntimeException
virtual_thread1
和virtual_thread2
的状态转换略图如所示:其中虚拟线程的
NEW
,STARTED
等状态暂忽略不表示。从上图可,virtual_thread2的状态为RUNNING时,platform thread的状态确是RUNNABLE,这点从jstack打印的栈信息也可以看出来。这是因为执行
sel.select()
时实际上是不需要cpu资源的,所以这时platform thread
的状态为RUNNABLE
测试代码设计思路:
NIO
模型保证virtual_thread2
一旦竞争上platform thread
就不会再unmount
下该线程。LockSupport#park()
和LockSupport#unpark()
让virtual_thread1
进入WAITING
和RUNNABLE
状态。CountDownLatch
让main thread
等待virtual_thread2
mount
上platform thread
virtual_thread1
的状态转换过程图(详细)VirtualThread
的所有状态转换可在附录看到。关于
VirtualThread
的getState()
方法可在附录的virtualThread
类的状态图部分中看到。实际执行流程图
GetStackTraceWhenRunnable
在KonaFiber
和Loom
里具体的调用过程可在详细调用流程图该链接中查看。以下为缩略图展示:
执行测试
修改代码使得测试在KonaFiber中适配
根据
GetStackTraceWhenRunnable.java
在@test
内的注释要求:我做了如下配置
使用Tencent Kona v8.0.13-GA for Fiber作为Java SDK
在
vm Options
里设置-Djdk.defaultScheduler.parallelism=1
。KonaFiber
没有virtualThreadScheduler.maxPoolSize=1
的相关配置,但是使用defaultScheduler.parallelism=1
也可以起到限制线程数只有1
个的作用该部分具体代码可在
VirtualThread
创建默认scheduler
时看到loom
KonaFiber
defaultScheduler.parallelism=1的解释
简单来说,
parallelism
参数表明并行级别,ForkJoinPool
根据该值来设定工作线程的数量。默认是cpu
的核心数。这里手动设置成一,就相当于设置了只有一个线程。关于
ForkJoinPool
的一些个人想法和了解:无论是
Loom
还是KonaFiber
都是用的ForkJoinPool
作为线程池,我认为这样做是一种共赢的决策。ForkJoinPool
Fork/Join
是一种支持分治任务模型的并行计算框架,它将任务分解成多个子任务(Fork
),最后再将各个子任务的结果进行合并(Join
)。ForkJoinPool
是Fork/Join
框架下的线程池类,它可以管理Fork/Join
任务的线程。VirtualThread
VirtualThread
封装了Continuation
成为了一个协程,突破了线程数量受到现有内存大小的限制,甚至于可以创建数百万的协程。1+ 1 > 2
ForkJoinPool
可以将任务分治成多个子任务,达到可以并行计算一个大任务的作用VirtualThread
的数量非常多,可以同时执行这些子任务多核CPU
的能力ForkJoinPool的
一些知识:int parallelism
:并行级别,ForkJoinPool
根据该值来设定工作线程的数量。默认是cpu
的核心数ForkJoinWorkerThreadFactory factory
:通过该factory
来创建线程UncaughtExceptionHandler handler
:设定异常处理器,用来处理任务在运行中发生的错误boolean asyncMode
:设定队列的工作模式,asyncMode=true
时:FIFO
队列;asyncMode=false
时:LIFO
栈Deque
中窃取工作任务。ForkJoinPool
在内部维护了多个任务队列,每当用户通过ForkJoinPool
的invoke()/submit()
提交任务后,ForkJoinPool
都会根据一定的路由规则将其分配到一个任务队列中,如果任务在执行过程中fork
出了子任务,就将子任务提交到该任务所在线程的任务队列中预期行为(Loom的行为)
正常打印调用栈信息
实际行为
经修改适配
KonaFiber
代码后使用jtreg
测试2分钟后报告超时。如果尝试直接执行该测试,则会发现该测试一直在执行,不会结束。
如何复现
jtreg
测试,修改@test
里的参数为@run main/othervm -Djdk.defaultScheduler.parallelism=1 GetStackTraceWhenRunnable
VM options
里添加-Djdk.defaultScheduler.parallelism=1
问题分析
简要介绍
本次问题的最终原因定位在
loom
和konaFiber
在java.lang.VirtualThread#tryGetStackTrace
实现上的差异。以下为逻辑分析,代码层面分析在后面会提及。
在
loom
中的实现当
virtual thread
的状态为unmounted
时会返回一个stack trace
,当virtual thread
为runnable
状态就属于这种情况,所以loom
最终可以成功打印出信息。在
konaFiber
的实现中当
virtual thread
的状态为newly created, parked, or terminated
时才会返回stack tracke
,virtual thread
为runnable
状态就不属于这三种状态,因此会返回null
。而在该方法的上层调用者java.lang.VirtualThread#getStackTrace
中,当tryGetStackTrace()
方法返回null
时会陷入一个死循环中。另外可以在附录中看到
virtualThread
类的状态图。loom
像在详细调用流程图中所绘,最终会走到
java.lang.VirtualThread#asyncGetStackTrace
,调用java.lang.VirtualThread#tryGetStackTrace
,之后调用jdk.internal.vm.Continuation#getStackTrace
返回一个栈信息因为
virtual_thread2
现在占据着platform thread
,所以virtual_thread1
的carrierThread
一定为null
,故最后会执行tryGetStackTrace();
在第一处注释会判断
virtual_Thread1
是否处于RUNNABLE
virtual_Thread1
的状态加一个暂停标志,以防状态发生改变。try
里的cont.getStackTrace()
返回栈信息virtual_Thread1
的状态在获取栈信息时是否发生改变,如果没有发生改变,将原状态恢复。virtual_Thread1
是RUNNABLE
或者可以恢复到RUNNABLE
状态,则让contionuation
继续执行任务在这段代码里
compareAndSetState(initialState, suspendedState)
是一个CAS操作,一气呵成地尝试将virtual_thread1
从initialState
改为suspendedState
。如果成功更改就返回true
在这里返回
continuation
的栈信息KonaFiber
像在详细调用流程图中所绘,最终会走到
java.lang.VirtualThread#getStackTrace
方法中,然后在该方法中调用java.lang.VirtualThread#tryGetStackTrace()
方法。如以下所示:
这段代码里
Thread.currentThread()
是main thread
,故会执行else语句,之后会在do -while循环里判断当前的virtual_thread1
是否有carrierThread
,如果没有就调用tryGetStackTrace()
,尝试获取调用栈信息。之后tryGetStackTrace()
会返回null
。之后为了防止该循环一直在跑占据cpu
资源,所以需要yield()
一下,释放下cpu
资源>最终该方法会一直在执行
do - while
循环直到virtual_thread1
mount
上一个platform thread
或者在tryGetStackTrace()
方法里获取到栈信息。的状态变更为
RUNNING
(mount
上platform thread
)。compareAndSetState(PARKED, PARKED_SUSPENDED)
是一个CAS
操作,尝试将virtual_thread1
的状态为PARKED
时,将其设置为PARKED_SUSPENDED
。但此时
virtual_thread1
的状态为RUNNABLE
,所以会返回false
,从而执行到else
语句里。之后最终会在注释处返回一个null
,从而导致java.lang.VirtualThread#getStackTrace
一直在做do - while
循环对
CAS
操作的解释:CAS
操作包括三个运算符V
A
B
CAS
的操作:会用A
与V
地址存放的值进行比较,如果相等(说明没有变化)就将V
地址存放的值设为B
。否则不做任何操作cas
操作不一定每次都会成功,所以就需要不断的重试,因此cas操
作常写为循环CAS
这里的
CAS
操作,类似于加上一把锁,保证了在获取cont
栈信息的时候,virtual_thread1
的状态不会发生改变。如何打破Do - While循环?
在
java.lang.VirtualThread#getStackTrace
中,会陷入死循环的原因是virtual_thread1
没有carrierthread,故只能执行为什么会产生这种差异性?
这种差异性出现的原因可能是二者在
java.lang.VirtualThread#tryGetStackTrace
的实现不同,因为一个RUNNABLE
的协程只要有空闲的platform
就可以变为RUNNING
,这样如果直接获取栈信息,就有可能拿到错误信息,所以需要对处于RUNNABLE
状态的VirtualThread
加以控制。loom
中是当VirtualThread
处于RUNNABLE
或者PARKED
两种状态中,都进行这种加锁操作来获取栈信息。同时这里会发现
loom
比KonaFiber
还多了一个STARTED
状态的判断,但是关于这个状态首先是二者一个是返回空栈信息,一个是返回null
,在信息内容上基本区别不大,其次是处于该状态的VirtualThread
本身就不会有调用栈信息。而
KonaFiber
中是当VirtualThread
处于PARKER
状态中才进行加锁操作来获取栈信息。RUNNING
状态的virtualThread
获取栈信息这里也会有疑问,处于
RUNNING
状态的virtualThread
如何获取栈信息,这就要看tryGetStackTrace
的调用者了loom
中的调用者是asyncGetStackTrace()
,可以看到它会直接获取其carrierThread
的栈信息再查看该方法,发现会直接尝试获取栈信息,若获取结果为null,就返回null;若获取空栈就返回空栈,否则就将结果返回。
KonaFiber
的调用者是getStackTrace()
,发现当VirtualThrea
的状态为RUNNING
时,其应该有carrierThread
,故会调用tryGetStackTrace(carrier);
来尝试获取栈信息。发现
tryGetStackTrace()
该方法会首先确认当前的carrierThread和形参carrier不是一个对象,这里有些不好理解,稍后会进一步讨论。之后会将carrier的状态暂停
将判断当前
VirtualThread
对象的carrierThread
就是形参carrier
并且,当前virtualThread
对象不处于PARKING
状态,当这两个条件都满足后就获取栈信息。carrierThread == carrier
:判断当前VirtualThread
对象的carrierThread
就是形参carrier
,这一步确认传递过来的carrier线程就是自己的carrier线程,因为其他线程的会无法打印。state() != PARKING
:判断当前VirtualThread
对象的状态不是PARKING
,因为PARKING
状态是是一种中间变量,很快就会变化,如果park
成功就会变为PARKED
,如果park
失败就会变为PINNED
状态。获取栈信息之后要将
carrier
的状态恢复。可能的解决方案
因为该测试的情况属于极端情况,不是正常情况,因为正常情况下一个处于
RUNNABLE
状态的virtualThread
肯定会很快获取到platform thread
来执行任务。所以不进行改进可能也是可以的。如果改进的话,也许可以考虑参考
loom
的实现,改为以下:定义initialState状态标识当前
virtualThread
的状态,当状态为RUNNABLE
,PARKED
时加锁来获取栈信息,并在获取后解锁。同时如果
virtualThread
的initialState
是RUNNABLE
,也要将其重新run()
一下。附录
环境信息
使用
jinfo -sysprops [pid]
命令查看的java系统信息,以下只列出重要信息,完整信息在附件中查看。IDE:IntelliJ IDEA 2023.2 (Ultimate Edition)
virtualThread
类的状态图示:VirtualThread和Thread的状态对应关系
完整jstack信息
完整jinfo sysprops信息
/
Beta Was this translation helpful? Give feedback.
All reactions