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]