A thread created suspended publishes its state and its suspend count in TWO
separate lock scopes:
{ lock; state_ = kSuspended; notify_all(); } // lock released here
if (create_suspended) { lock; suspend_count_ = 1; wait(count == 0); }
and Resume() does WaitStarted() - which waits only for state_ != kUninitialized -
followed by `if (suspend_count_ == 0) return false;`. So a resumer can slip into
the gap: it sees the thread started, sees suspend_count_ still 0, drops the
resume and returns false. The new thread then sets the count to 1 and waits on it
forever. A textbook lost wakeup.
Measured in Project Sylpheed. Pressing (A) on the title makes the game do
XamUserGetXUID -> NtCreateEvent -> ExCreateThread(entry=821748F0,
CREATE_SUSPENDED) -> NtResumeThread, and the loader thread then never ran: zero
kernel calls of its own (it appeared in the log only as an argument) and 00:00:00
host CPU time, while the emulator sat at 546% CPU. Boots reached the main menu
1 time in 6.
Fixed by publishing state_ and suspend_count_ under one lock and waiting without
releasing it, so a resumer past WaitStarted() always observes 1.
On the first clean boot after the fix the same loader thread is the CALLER on 20
kernel-call lines and issues 4 ResolvePath asset reads. Every failed boot before
it had exactly zero of both.
Also logs when the host resume is refused. XThread::Resume's Linux path
discarded that bool - the Windows path turns it into X_STATUS_UNSUCCESSFUL - so a
dropped resume was invisible from both sides. Note the log is not by itself a
defect: resuming a thread that is not suspended legitimately returns false, and
it fires ~7 times in a normal boot.
(cherry picked from commit a60fe7d11c)