This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 713ad01e16 perf: add Thread.onSpinWait() to CAS spin loops (#3149)
713ad01e16 is described below

commit 713ad01e16cb1e5b30902697a67b3d9e1863ffb7
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Tue Jun 23 19:57:00 2026 +0800

    perf: add Thread.onSpinWait() to CAS spin loops (#3149)
    
    * perf: add Thread.onSpinWait() to CAS spin loops
    
    Motivation:
    CAS retry loops in AbstractDispatcher and Mailbox spin without any
    CPU hint when contention causes compareAndSet to fail. JDK 9's
    Thread.onSpinWait() hints to the CPU that we're in a spin-wait
    loop, improving power efficiency and reducing context-switch
    latency.
    
    Modification:
    - AbstractDispatcher.scala: add onSpinWait() in shutdown schedule
      CAS retry loop body
    - Mailbox.scala: convert setAsScheduled() and setAsIdle() from
      @tailrec recursive CAS loops to while loops with onSpinWait(),
      re-reading currentStatus on each retry
    
    Result:
    Better CPU behavior under contention on the message dispatch hot
    path (setAsScheduled is called on every registerForExecution).
    The onSpinWait pattern is already established in the codebase
    (AbstractNodeQueue, AbstractBoundedNodeQueue, AffinityPool).
    
    Tests:
    sbt "actor/compile" — passed
    
    References:
    Refs #3136
    
    * perf: extend Thread.onSpinWait() to remaining CAS spin loops
    
    Motivation:
    The initial PR covered three CAS spin loops in AbstractDispatcher and
    Mailbox. Internal review identified four additional @tailrec CAS loops
    that spin on compareAndSet failure without any CPU hint, which can
    waste power and slow down SMT siblings under contention:
    
    - `AbstractDispatcher.ifSensibleToDoSoThenScheduleShutdown` (2 CAS
      failure paths), on the detach and taskCleanup paths where shutdown
      contention is realistic.
    - `Mailbox.resume`, `suspend`, `becomeClosed`, called during actor
      suspend/resume/stop.
    
    Modification:
    Rewrite each @tailrec method into an explicit while loop and insert
    `Thread.onSpinWait()` between the failed CAS and the volatile re-read.
    `Thread.onSpinWait()` is a JDK 9+ CPU hint (x86 PAUSE / ARM YIELD)
    that neither yields to the OS scheduler nor introduces a delay — it
    actually reduces starvation rather than increasing it.
    
    Result:
    Lower power draw and faster CAS recovery under contention on the
    dispatcher shutdown and mailbox status-change paths. MiMa clean —
    method signatures are unchanged and all affected methods are private.
    
    Tests:
    sbt "actor/compile" -- passed
    sbt "actor-tests/Test/testOnly ActorMailboxSpec" -- 33/33 passed
    sbt "actor-tests/Test/testOnly DispatcherShutdownSpec" -- 1/1 passed
    sbt "actor-tests/Test/testOnly DispatcherActorsSpec" -- 1/1 passed
    sbt "actor/mimaReportBinaryIssues" -- no issues
    
    References:
    Discovered during internal review of PR #3149.
    
    * refactor: address code review feedback in CAS spin loops
    
    Motivation:
    Code review feedback on readability and dead code concerns
    in the CAS retry loops.
    
    Modification:
    - Rewrite setAsScheduled from while({block}) to while(true) style
      for consistency with other CAS loops
    - Replace trailing 'false // unreachable' with explicit
      'throw new IllegalStateException("unreachable")'
    - Fix misleading exception message from 'actor class marker' to
      'shutdown schedule marker' in AbstractDispatcher
    
    Result:
    More consistent and readable CAS loop implementations with proper
    dead code handling.
    
    References:
    Refs #3149
---
 .../apache/pekko/dispatch/AbstractDispatcher.scala | 30 ++++----
 .../scala/org/apache/pekko/dispatch/Mailbox.scala  | 80 ++++++++++++++--------
 2 files changed, 67 insertions(+), 43 deletions(-)

diff --git 
a/actor/src/main/scala/org/apache/pekko/dispatch/AbstractDispatcher.scala 
b/actor/src/main/scala/org/apache/pekko/dispatch/AbstractDispatcher.scala
index f5e9e277dc..08a4b16767 100644
--- a/actor/src/main/scala/org/apache/pekko/dispatch/AbstractDispatcher.scala
+++ b/actor/src/main/scala/org/apache/pekko/dispatch/AbstractDispatcher.scala
@@ -190,18 +190,22 @@ abstract class MessageDispatcher(val configurator: 
MessageDispatcherConfigurator
     case _                    => eventStream.publish(Error(t, 
getClass.getName, getClass, t.getMessage))
   }
 
-  @tailrec
   private final def ifSensibleToDoSoThenScheduleShutdown(): Unit = {
-    if (inhabitants <= 0) shutdownSchedule match {
-      case UNSCHEDULED =>
-        if (updateShutdownSchedule(UNSCHEDULED, SCHEDULED)) 
scheduleShutdownAction()
-        else ifSensibleToDoSoThenScheduleShutdown()
-      case SCHEDULED =>
-        if (updateShutdownSchedule(SCHEDULED, RESCHEDULED)) ()
-        else ifSensibleToDoSoThenScheduleShutdown()
-      case RESCHEDULED =>
-      case unexpected  =>
-        throw new IllegalArgumentException(s"Unexpected actor class marker: 
$unexpected") // will not happen, for exhaustiveness check
+    while (inhabitants <= 0) {
+      shutdownSchedule match {
+        case UNSCHEDULED =>
+          if (updateShutdownSchedule(UNSCHEDULED, SCHEDULED)) {
+            scheduleShutdownAction()
+            return
+          }
+        case SCHEDULED =>
+          if (updateShutdownSchedule(SCHEDULED, RESCHEDULED)) return
+        case RESCHEDULED =>
+          return
+        case unexpected =>
+          throw new IllegalArgumentException(s"Unexpected shutdown schedule 
marker: $unexpected") // will not happen, for exhaustiveness check
+      }
+      Thread.onSpinWait()
     }
   }
 
@@ -255,14 +259,14 @@ abstract class MessageDispatcher(val configurator: 
MessageDispatcherConfigurator
           try {
             if (inhabitants == 0) shutdown() // Warning, racy
           } finally {
-            while (!updateShutdownSchedule(shutdownSchedule, UNSCHEDULED)) {}
+            while (!updateShutdownSchedule(shutdownSchedule, UNSCHEDULED)) { 
Thread.onSpinWait() }
           }
         case RESCHEDULED =>
           if (updateShutdownSchedule(RESCHEDULED, SCHEDULED)) 
scheduleShutdownAction()
           else run()
         case UNSCHEDULED =>
         case unexpected  =>
-          throw new IllegalArgumentException(s"Unexpected actor class marker: 
$unexpected") // will not happen, for exhaustiveness check
+          throw new IllegalArgumentException(s"Unexpected shutdown schedule 
marker: $unexpected") // will not happen, for exhaustiveness check
       }
     }
   }
diff --git a/actor/src/main/scala/org/apache/pekko/dispatch/Mailbox.scala 
b/actor/src/main/scala/org/apache/pekko/dispatch/Mailbox.scala
index af728fabf9..03ebf96a50 100644
--- a/actor/src/main/scala/org/apache/pekko/dispatch/Mailbox.scala
+++ b/actor/src/main/scala/org/apache/pekko/dispatch/Mailbox.scala
@@ -150,14 +150,19 @@ private[pekko] abstract class Mailbox(val messageQueue: 
MessageQueue)
    *
    * @return true if the suspend count reached zero
    */
-  @tailrec
-  final def resume(): Boolean = currentStatus match {
-    case Closed =>
-      setStatus(Closed); false
-    case s =>
+  final def resume(): Boolean = {
+    var s = currentStatus
+    while (true) {
+      if (s == Closed) {
+        setStatus(Closed)
+        return false
+      }
       val next = if (s < suspendUnit) s else s - suspendUnit
-      if (updateStatus(s, next)) next < suspendUnit
-      else resume()
+      if (updateStatus(s, next)) return next < suspendUnit
+      Thread.onSpinWait()
+      s = currentStatus
+    }
+    throw new IllegalStateException("unreachable")
   }
 
   /**
@@ -166,47 +171,62 @@ private[pekko] abstract class Mailbox(val messageQueue: 
MessageQueue)
    *
    * @return true if the previous suspend count was zero
    */
-  @tailrec
-  final def suspend(): Boolean = currentStatus match {
-    case Closed =>
-      setStatus(Closed); false
-    case s =>
-      if (updateStatus(s, s + suspendUnit)) s < suspendUnit
-      else suspend()
+  final def suspend(): Boolean = {
+    var s = currentStatus
+    while (true) {
+      if (s == Closed) {
+        setStatus(Closed)
+        return false
+      }
+      if (updateStatus(s, s + suspendUnit)) return s < suspendUnit
+      Thread.onSpinWait()
+      s = currentStatus
+    }
+    throw new IllegalStateException("unreachable")
   }
 
   /**
    * set new primary status Closed. Caller does not need to worry about whether
    * status was Scheduled or not.
    */
-  @tailrec
-  final def becomeClosed(): Boolean = currentStatus match {
-    case Closed =>
-      setStatus(Closed); false
-    case s => updateStatus(s, Closed) || becomeClosed()
+  final def becomeClosed(): Boolean = {
+    var s = currentStatus
+    while (true) {
+      if (s == Closed) {
+        setStatus(Closed)
+        return false
+      }
+      if (updateStatus(s, Closed)) return true
+      Thread.onSpinWait()
+      s = currentStatus
+    }
+    throw new IllegalStateException("unreachable")
   }
 
   /**
    * Set Scheduled status, keeping primary status as is.
    */
-  @tailrec
   final def setAsScheduled(): Boolean = {
-    val s = currentStatus
-    /*
-     * Only try to add Scheduled bit if pure Open/Suspended, not Closed or with
-     * Scheduled bit already set.
-     */
-    if ((s & shouldScheduleMask) != Open) false
-    else updateStatus(s, s | Scheduled) || setAsScheduled()
+    var s = currentStatus
+    while (true) {
+      if ((s & shouldScheduleMask) != Open) return false
+      if (updateStatus(s, s | Scheduled)) return true
+      Thread.onSpinWait()
+      s = currentStatus
+    }
+    throw new IllegalStateException("unreachable")
   }
 
   /**
    * Reset Scheduled status, keeping primary status as is.
    */
-  @tailrec
   final def setAsIdle(): Boolean = {
-    val s = currentStatus
-    updateStatus(s, s & ~Scheduled) || setAsIdle()
+    var s = currentStatus
+    while (!updateStatus(s, s & ~Scheduled)) {
+      Thread.onSpinWait()
+      s = currentStatus
+    }
+    true
   }
   /*
    * AtomicReferenceFieldUpdater for system queue.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to