hitHuang commented on issue #19370:
URL: https://github.com/apache/nuttx/issues/19370#issuecomment-4925984121

   Two mechanisms combine to produce the imbalance — this is a statistical 
effect, not a scheduler bias toward either thread.
   
     First, the mutex's fast path is scheduler-transparent. With 
pthread_mutex_init(&g_mutex, NULL), the protocol is SEM_PRIO_NONE, so 
nxsem_wait()/nxsem_post() (sem_wait.c:170-199, sem_post.c:147-176) use 
atomic_try_cmpxchg when uncontended — plain
     atomic ops, no interrupt masking, no preemption disable. The scheduler 
cannot tell whether a thread is inside the critical section or not, so a timer 
tick can land there just as easily as anywhere else.
   
     Second, nxsched_process_roundrobin() (sched_roundrobin.c) always resets 
the expiring thread's timeslice back to full before checking whether the next 
same-priority thread should actually get the CPU. Refilling the timeslice and 
switching are
     separate steps; the first always happens, the second is conditional.
   
     Combined: if a timeslice expiry lands exactly while the running thread 
holds the mutex, the scheduler still switches — it has no visibility into lock 
state. The incoming thread immediately fails the mutex fast path, falls into 
the slow path, and
     blocks. The CPU falls straight back to the thread that was just switched 
out — which already got its timeslice reset to full. It effectively gets a 
whole extra timeslice for free, while the other thread contributes nothing this 
round and must
     requeue for the next expiry.
   
     Since the loop body is just lock/increment/unlock with no other work, the 
critical section occupies a non-trivial fraction of each iteration, so this 
collision isn't rare. With CONFIG_RR_INTERVAL=200 over 5 seconds there are only 
~25 expiry checks
     total; a handful of collisions favoring the same thread is enough to 
snowball into the reported 10%-37% gap. Shortening the timeslice increases the 
sample count and narrows the skew statistically, but doesn't remove the 
mechanism.
   
     Worked example
   
     CONFIG_RR_INTERVAL=200, 1ms tick. A = low_task1, B = low_task2, equal 
priority.
   
     t=0: A runs, B queued behind it, A's timeslice = 200.
   
     t=200: A's timeslice expires. Scheduler resets it to 200, sees B is next 
with equal priority, switches to B. A requeues behind B.
   
     t=200~400: B runs exclusively. Just before t=400, B has just acquired the 
lock but hasn't unlocked yet — mutex holder is B.
   
     t=400: B's timeslice expires (lock state isn't checked). Scheduler resets 
B's timeslice to 200, sees A is next, switches to A.
   
     t=400+ε: A resumes, immediately calls lock. Fast path sees B as holder, 
CAS fails, A falls to the slow path, blocks, and is removed from the ready 
queue. B is now the only runnable thread, so the CPU switches straight back to 
B.
   
     t=400+ε~600+ε: B finishes its count2++/unlock, hands the mutex to A and 
requeues A (no preemption on equal priority — A doesn't get the CPU yet), then 
keeps running lock/count2++/unlock until its timeslice (reset at t=400) runs 
out again, around
     t=600.
   
     t≈600+ε: Scheduler switches to A, which finally runs a clean full 
timeslice.
   
     Net effect: B runs continuously from t=200 to ~t=600 — almost 400ms, two 
timeslices back to back — while A is skipped for a full round, only touching 
the CPU long enough to fail one lock attempt. One such collision hands B a full 
extra timeslice
     at A's expense; a few of these across ~25 expiry checks in 5 seconds 
account for the observed imbalance.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to