libusb: Fix high CPU usage in callback thread #783
No reviewers
Labels
No labels
3rdparty
Android
API
Author does not respond
bsd
bug
build system/CI
ci-virtual-device
CMake
Contributions Welcome
Core
dependencies
device-specific
documentation
don't_merge
duplicate
enhancement
Example
Haiku
hidraw
hotplug
info required
invalid
libusb
macOS
Misc
Not an issue
question
Release
Test
Test GUI
Windows
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-libusb/hidapi!783
Loading…
Reference in a new issue
No description provided.
Delete branch "copilot/optimize-hidapi-thread-wait"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Callback_thread used hidapi_thread_cond_timedwait with an absolute
timespec already in the past, causing a tight spin loop. Replaced
with cond_wait + predicate pattern so the thread sleeps until an
event or a shutdown signal arrives.
Multithreading fixes folded in along the way:
natural condvar partner), with signal-under-mutex to avoid a
lost-wakeup race at shutdown.
to match hid_hotplug_register_callback()'s HID_API_HOTPLUG_ENUMERATE
traversal of hid_hotplug_context.devs — fixes a use-after-free
where a concurrent disconnect could free a list node mid-traversal.
hidapi_thread_join) so every access to devs happens under
hid_hotplug_context.mutex.
libusb_hotplug_register_callback() error path, which previously
leaked.
Closes: #782
Assisted-by: Copilot:claude-sonnet-4.6
Assisted-by: Copilot:claude-opus-4.6
Assisted-by: Claude:claude-opus-4.7
@ -33,4 +33,6 @@ doxygen/html/# Visual Studio Code + CMake.vscode/@copilot why do we need to send a signal with mutex locked?
It prevents a lost wakeup that would cause a deadlock. Without the mutex, there's a race between the predicate check (line 1223) and
cond_wait(line 1224):callback_threadevaluateshotplug_cbsas non-NULL → enters inner while bodyhotplug_cbs = NULLhotplug_threadloop exits, sendscond_signal→ no one is waiting yet, signal is lostcallback_threadenterscond_wait→ blocks foreverhidapi_thread_joinat line 1279 → deadlockHolding the mutex ensures the signal is either delivered before
cond_wait(predicate re-checked on next iteration) or after (thread wakes up).@copilot+claude-sonnet-4.6 We used to handle all evens in a queue while holding the lock on a mutex, and now we lock it only when manipulating the queue, but not when handling the event. Is there a benefit in that approach?
@copilot+claude-opus-4.6 Shouldn't we also ensure the exit condition is set? I.e.
hid_hotplug_context.hotplug_cbs = NULL? If it is already meet - need a hint/comment about that (but preferably set that condition explicitly).The exit condition is already met: the
while (hid_hotplug_context.hotplug_cbs)loop inhotplug_threadonly exits whenhotplug_cbsis NULL, so by the time we reach that signal it is guaranteed to be NULL. The signal is needed solely to wakecallback_threadfromcond_waitso it can re-evaluate the (now-false) predicate and exit.Added a comment to make this explicit in
5d7a7c6.Yes — releasing the lock during event processing avoids two problems:
Blocking the producer:
hid_libusb_hotplug_callback(which runs onhotplug_thread) must acquire the same mutex to enqueue events. If we held the lock during the (potentially long) callback invocation, the producer would stall for that entire duration. Releasing it lets new events be enqueued concurrently while an older one is being dispatched.Deadlock prevention: If user callback code calls back into HIDAPI (e.g.,
hid_hotplug_deregister_callback), that call will attempt to acquirecallback_thread.mutex. Holding it during processing would deadlock.The tradeoff is that
hid_hotplug_context.devs(the known-device list maintained insideprocess_hotplug_event) is modified without the lock, but that is safe because onlycallback_threadreads or writesdevs— events are still processed serially within the single consumer thread.@copilot+claude-sonnet-4.6 I also see that
devsis accessed fromhid_hotplug_register_callback, even though it is done before the thread is created, maybe move that initialisation inside of the thread (to match cleanup of that variable at the end of the thread).And would be great to explicitly mark that variable that it should only be accessed from that thread.
Done in
3f4721e.devsis now initialized at the start ofcallback_thread(paired with its cleanup at the end), and its struct comment marks it as "Only accessed from callback_thread." TheHID_API_HOTPLUG_ENUMERATEcase inhid_hotplug_register_callbacknow enumerates devices locally (freed after use) so it never touches the thread-owneddevs.@CalcProgrammer1 could you check this change in you environment regarding high CPU usage? it should give even better result.
I think this PR has more than enought changes already. Will merge it like this and going to review/update more of this using Claude - it already spotted potential races and UB in current implementation.