Repository navigation
Fix livelock when all TaskGroup runqueues are full (#1038) #3633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c7432d8
Fix livelock when all TaskGroup runqueues are full (#1038)
two-headBoy 283e3f4
Fix race by deferring push to remained callback in push_rq
two-headBoy 6779591
Fix push_rq spinning on pthread-mode task at runqueue bottom
two-headBoy 209fac5
Document why the held-task array is bounded
two-headBoy 852f6f6
Fix recursive sched_to in push_rq remained callbacks
two-headBoy 6bad7b2
Fix stale TaskGroup usage after push_rq context switches
two-headBoy 51559ab
Fix push_rq livelock by stashing a task in remained callbacks
two-headBoy e87d9f2
Add regression test for push_rq runqueue-full livelock
two-headBoy ad6e8fb
Fix pending task reuse after failed runqueue pop
two-headBoy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,22 +83,138 @@ inline void TaskGroup::sched_to(TaskGroup** pg, bthread_t next_tid) { | |
| } | ||
|
|
||
| inline void TaskGroup::push_rq(bthread_t tid) { | ||
| while (!_rq.push(tid)) { | ||
| TaskGroup* g = this; | ||
| while (!g->_rq.push(tid)) { | ||
| // Created too many bthreads: a promising approach is to insert the | ||
| // task into another TaskGroup, but we don't use it because: | ||
| // * There're already many bthreads to run, inserting the bthread | ||
| // into other TaskGroup does not help. | ||
| // * Insertions into other TaskGroups perform worse when all workers | ||
| // are busy at creating bthreads (proved by test_input_messenger in | ||
| // brpc) | ||
| flush_nosignal_tasks(); | ||
| LOG_EVERY_SECOND(ERROR) << "_rq is full, capacity=" << _rq.capacity(); | ||
| // TODO(gejun): May cause deadlock when all workers are spinning here. | ||
| // A better solution is to pop and run existing bthreads, however which | ||
| // make set_remained()-callbacks do context switches and need extensive | ||
| // reviews on related code. | ||
| ::usleep(1000); | ||
| g->flush_nosignal_tasks(); | ||
| LOG_EVERY_SECOND(ERROR) << "_rq is full, capacity=" << g->_rq.capacity(); | ||
| const auto pop_task = [g](bthread_t* t) -> bool { | ||
| #ifndef BTHREAD_FAIR_WSQ | ||
| return g->_rq.pop(t); | ||
| #else | ||
| return g->_rq.steal(t); | ||
| #endif | ||
| }; | ||
| if (g->_sched_recursive_guard != 0) { | ||
| // Remained callbacks run with the guard held, either in | ||
| // sched_to()'s remained loop or at task_runner() entry. Re-entering | ||
| // sched_to() here would be recursive, while waiting here can | ||
| // livelock if every worker is running a remained callback. Make | ||
| // room for |tid| and requeue the popped task after the guard is | ||
| // balanced. | ||
| CHECK(g->_pending_rq_tid == INVALID_BTHREAD); | ||
| bthread_t pending_tid; | ||
| if (!pop_task(&pending_tid)) { | ||
| continue; | ||
| } | ||
| g->_pending_rq_tid = pending_tid; | ||
| const bool pushed = g->_rq.push(tid); | ||
| CHECK(pushed) << "push_rq: failed to push after popping a task"; | ||
| return; | ||
| } | ||
| if (g->is_current_pthread_task() && !g->is_current_main_task()) { | ||
| // A pthread-mode bthread runs on _main_stack and cannot be | ||
| // scheduled out, fall back to waiting. | ||
| ::usleep(1000); | ||
| continue; | ||
| } | ||
| bthread_t run_tid = 0; | ||
| if (!pop_task(&run_tid)) { | ||
| // The queue was drained by stealers between the failed push and | ||
| // the pop, retrying the push will succeed. | ||
| continue; | ||
| } | ||
| if (g->is_current_main_task() && | ||
| address_meta(run_tid)->attr.stack_type == BTHREAD_STACKTYPE_PTHREAD) { | ||
| // pthread-mode tasks are run inline on _main_stack by the | ||
| // dispatch loops (run_main_task/task_runner). Switching to one | ||
| // from the main task itself causes no stack jump and would | ||
| // silently drop it, so it cannot be run here. But pushing it | ||
| // straight back makes the LIFO pop() return this same task | ||
| // forever, hiding any runnable task sitting deeper in the | ||
| // queue. Hold pthread-mode tasks aside and keep popping until | ||
| // a runnable one shows up. | ||
| // Bounded budget: the array must live on the worker's stack | ||
| // (no allocation on a resource-exhaustion path), and held | ||
| // tasks are invisible to stealers, so keeping the list short | ||
| // limits how long they can be delayed. 16 * sizeof(bthread_t) | ||
| // == 128 bytes == two cache lines. | ||
| bthread_t pending[16]; | ||
| int npending = 0; | ||
| pending[npending++] = run_tid; | ||
| bool found = false; | ||
| while (npending < 16 && pop_task(&run_tid)) { | ||
| if (address_meta(run_tid)->attr.stack_type != | ||
| BTHREAD_STACKTYPE_PTHREAD) { | ||
| found = true; | ||
| break; | ||
| } | ||
| pending[npending++] = run_tid; | ||
| } | ||
|
two-headBoy marked this conversation as resolved.
Resolved
|
||
| // Push the held tasks back. The pops above freed at least | ||
| // |npending| slots, _rq has a single producer (this worker) | ||
| // and stealers never push, so these pushes must succeed. | ||
| // Reverse order restores their relative order in the queue. | ||
| for (int i = npending - 1; i >= 0; --i) { | ||
| const bool pushed = g->_rq.push(pending[i]); | ||
| CHECK(pushed) << "push_rq: failed to put back held task"; | ||
| } | ||
| if (!found) { | ||
| // Everything within reach is pthread-mode: wait for | ||
| // stealers to drain the queue. | ||
|
two-headBoy marked this conversation as resolved.
Resolved
|
||
| ::usleep(1000); | ||
|
two-headBoy marked this conversation as resolved.
Resolved
|
||
| continue; | ||
| } | ||
|
two-headBoy marked this conversation as resolved.
Resolved
|
||
| // Fall through: run_tid is a runnable task, run it inline | ||
| // below to free a slot for |tid|. | ||
| } | ||
| // Run the popped bthread in place instead of spinning, otherwise | ||
| // all workers may spin here and no one drains the queues any more. | ||
| // The pop also frees one slot, which guarantees that the deferred | ||
| // push of the current task succeeds: _rq has a single producer (the | ||
| // owning worker) and stealers never push. | ||
| // |args| lives on the stack that sched_to() is about to freeze and is | ||
| // read by the remained-callback after the switch, so it must stay in | ||
| // this scope: if it were declared inside the branch below, its slot | ||
| // could be reused before the callback has finished reading it. | ||
| ReadyToRunArgs args = { g->tag(), nullptr, false }; | ||
| if (!g->is_current_main_task()) { | ||
| // Preserve the task being scheduled out. The push is deferred to | ||
| // a remained-callback: publishing a still-running task into _rq | ||
| // lets a stealer jump into it and run the same context | ||
| // concurrently on another worker. The callback runs on the stack | ||
| // of |run_tid| after sched_to() has frozen the current context, | ||
| // same as exchange(). Capture the meta now, current_tid() is | ||
| // |run_tid| by the time the callback runs. | ||
| // The callback may re-enter push_rq() while _rq is still full; | ||
| // the guard branch above makes room without switching and defers | ||
| // the popped task until the guard is balanced. | ||
| args.meta = g->_cur_meta; | ||
| g->set_remained(ready_to_run_in_worker_ignoresignal, &args); | ||
| } | ||
| // The main task is never queued; ending_sched() resumes it via | ||
| // _main_tid when the queue drains. Jumping to a pthread-mode task | ||
| // from a normal bthread resumes the dispatch loop on _main_stack | ||
| // which runs it inline, same as ending_sched() does. | ||
| sched_to(&g, run_tid); | ||
|
two-headBoy marked this conversation as resolved.
Resolved
Comment on lines
+198
to
+205
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This switch makes bthread_start_background() an implicit scheduling point. Existing callers, including ExecutionQueueBase::start_execute(), deliberately use background submission because they may hold a pthread mutex. |
||
| // Resume here after the popped bthread (and possibly others) ran, | ||
|
two-headBoy marked this conversation as resolved.
Resolved
|
||
| // then retry the push of |tid|. | ||
| } | ||
| } | ||
|
|
||
| inline void TaskGroup::requeue_pending_task() { | ||
| if (_pending_rq_tid == INVALID_BTHREAD) { | ||
| return; | ||
| } | ||
| const bthread_t tid = _pending_rq_tid; | ||
| _pending_rq_tid = INVALID_BTHREAD; | ||
| push_rq(tid); | ||
| } | ||
|
|
||
| inline void TaskGroup::flush_nosignal_tasks_remote() { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
push_rq() can now suspend the caller and resume it on another worker. Updating the local pointer in ready_to_run() does not update its callers: after butex_wake_except() wakes many joiners, task_runner() still uses its old g to install _release_last_context and call ending_sched().