@@ -92,11 +92,6 @@ static void doHandshake(Object node) {
9292 SINGLETON .processHandshake ((Node ) node );
9393 }
9494
95- @ Override
96- protected void setFastPending (Thread t ) {
97- setVolatile (t , 1 );
98- }
99-
10095 @ Override
10196 @ TruffleBoundary
10297 public TruffleSafepointImpl getCurrent () {
@@ -110,21 +105,24 @@ public TruffleSafepointImpl getCurrent() {
110105
111106 @ Override
112107 protected void clearFastPending () {
113- setVolatile (Thread .currentThread (), 0 );
108+ Thread carrierThread = JAVA_LANG_ACCESS .currentCarrierThread ();
109+ long eetop = UNSAFE .getLongVolatile (carrierThread , THREAD_EETOP_OFFSET );
110+ UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , 0 );
114111 }
115112
116- private static void setVolatile (Thread thread , int value ) {
113+ @ Override
114+ protected void setFastPending (Thread thread ) {
117115 /*
118116 * The thread will not go away here because the Truffle implementation ensures that this
119117 * method is no longer used if the thread is no longer active. It only sets this state for
120118 * contexts that are currently entered on a thread. Being entered implies that the thread is
121119 * active.
122120 */
123- assert thread .isAlive () : "thread must remain alive while setting fast pending" ;
121+ assert thread .isAlive () : "thread must remain alive while setting the pending flag " ;
124122
125123 long eetop = UNSAFE .getLongVolatile (thread , THREAD_EETOP_OFFSET );
126124 if (eetop != 0 ) {
127- UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , value );
125+ UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , 1 );
128126 } else { // only the case for VirtualThreads
129127 Object carrierThread = UNSAFE .getObjectVolatile (thread , THREAD_CARRIER_THREAD_OFFSET );
130128 if (carrierThread == null ) {
@@ -137,7 +135,7 @@ private static void setVolatile(Thread thread, int value) {
137135 return ;
138136 }
139137 eetop = UNSAFE .getLongVolatile (carrierThread , THREAD_EETOP_OFFSET );
140- UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , value );
138+ UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , 1 );
141139 /*
142140 * If the VirtualThread moves to another carrier thread after this method returns, the
143141 * pending flag will still be set correctly thanks to setPendingFlagForVirtualThread()
@@ -150,15 +148,44 @@ private static void setVolatile(Thread thread, int value) {
150148 static void setPendingFlagForVirtualThread () {
151149 TruffleSafepointImpl safepoint = STATE .get ();
152150 if (safepoint != null ) {
153- boolean pending = safepoint .isFastPendingSet ();
154-
155- // VirtualThread#carrierThread is not set yet, it set after this hook is called.
156- // However, Thread.currentCarrierThread() is already set so we can use that.
157- // We could also get the carrier thread from the hook arguments but that seems more
158- // expensive.
159151 Thread carrierThread = JAVA_LANG_ACCESS .currentCarrierThread ();
160152 long eetop = UNSAFE .getLongVolatile (carrierThread , THREAD_EETOP_OFFSET );
161- UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , pending ? 1 : 0 );
153+
154+ /*
155+ * When this method and setFastPending() are run concurrently, there is a possibility
156+ * that we set the carrier pending flag to 0 here based on pendingBefore==false, after
157+ * setFastPending() sets the flag to 1 (which would lose the flag):
158+ *
159+ * @formatter:off
160+ * with VT=VirtualThread MT=the thread that submits the ThreadLocalAction:
161+ * VT: boolean pending = this.fastPendingSet; // false
162+ * MT: fastPendingSet = true;
163+ * MT: carrierThread.pending = true;
164+ * VT: carrierThread.pending = pending; // false
165+ * @formatter:on
166+ *
167+ * So we check for that case with pendingAfter and fix it. This is correct because
168+ * either we wrote the 0 before setFastPending() wrote the 1 (harmless), or we wrote it
169+ * after and then we must see pendingAfter==true, because (the caller of)
170+ * setFastPending() sets fastPendingSet before writing to the carrier pending flag.
171+ *
172+ * If this method and setFastPending() are not run concurrently, then it is as if both
173+ * methods were executed one after another (since all accesses are volatile and so
174+ * sequentially consistent) and it works trivially.
175+ *
176+ * This solution is better than `if (isFastPendingSet()) { carrierThread.pending = 1; }`
177+ * because that can leave the carrier flag as true after an unmount of a virtual thread
178+ * which did not process safepoints yet and the newly-mounted virtual thread would not
179+ * clear it, causing extra stub calls (clearing is only done if fastPendingSet is true,
180+ * necessary for DefaultThreadLocalHandshake where clearFastPending() is not
181+ * idempotent).
182+ */
183+ boolean pendingBefore = safepoint .isFastPendingSet ();
184+ UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , pendingBefore ? 1 : 0 );
185+ boolean pendingAfter = safepoint .isFastPendingSet ();
186+ if (!pendingBefore && pendingAfter ) {
187+ UNSAFE .putIntVolatile (null , eetop + PENDING_OFFSET , 1 );
188+ }
162189 }
163190 }
164191
0 commit comments