fix: prevent Task.get deadlocks from threadpool starvation (#6758)

This PR prevents deadlocks from non-cyclical task waits that may
otherwise occur during parallel elaboration with small threadpool sizes.
This commit is contained in:
Sebastian Ullrich 2025-01-23 16:01:39 -07:00 committed by GitHub
parent ebda2d4d25
commit 214093e6c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 7 deletions

View file

@ -26,7 +26,7 @@
"displayName": "Sanitize build config",
"cacheVariables": {
"LEAN_EXTRA_CXX_FLAGS": "-fsanitize=address,undefined",
"LEANC_EXTRA_FLAGS": "-fsanitize=address,undefined -fsanitize-link-c++-runtime",
"LEANC_EXTRA_CC_FLAGS": "-fsanitize=address,undefined -fsanitize-link-c++-runtime",
"SMALL_ALLOCATOR": "OFF",
"BSYMBOLIC": "OFF"
},

View file

@ -516,8 +516,17 @@ The tasks have an overridden representation in the runtime.
structure Task (α : Type u) : Type u where
/-- `Task.pure (a : α)` constructs a task that is already resolved with value `a`. -/
pure ::
/-- If `task : Task α` then `task.get : α` blocks the current thread until the
value is available, and then returns the result of the task. -/
/--
Blocks the current thread until the given task has finished execution, and then returns the result
of the task. If the current thread is itself executing a (non-dedicated) task, the maximum
threadpool size is temporarily increased by one while waiting so as to ensure the process cannot
be deadlocked by threadpool starvation. Note that when the current thread is unblocked, more tasks
than the configured threadpool size may temporarily be running at the same time until sufficiently
many tasks have finished.
`Task.map` and `Task.bind` should be preferred over `Task.get` for setting up task dependencies
where possible as they do not require temporarily growing the threadpool in this way.
-/
get : α
deriving Inhabited, Nonempty

View file

@ -693,10 +693,14 @@ class task_manager {
unique_lock<mutex> lock(m_mutex);
m_idle_std_workers++;
while (true) {
if (m_queues_size == 0) {
if (m_shutting_down) {
break;
}
if (m_queues_size == 0 && m_shutting_down) {
break;
}
if (m_queues_size == 0 ||
// If we have reached the maximum number of standard workers (because the
// maximum was decreased by `task_get`), wait for someone else to become
// idle before picking up new work.
m_std_workers.size() - m_idle_std_workers >= m_max_std_workers) {
m_queue_cv.wait(lock);
continue;
}
@ -859,7 +863,19 @@ public:
unique_lock<mutex> lock(m_mutex);
if (t->m_value)
return;
// see `Task.get`
bool in_pool = g_current_task_object && g_current_task_object->m_imp->m_prio <= LEAN_MAX_PRIO;
if (in_pool) {
m_max_std_workers++;
if (m_idle_std_workers == 0)
spawn_worker();
else
m_queue_cv.notify_one();
}
m_task_finished_cv.wait(lock, [&]() { return t->m_value != nullptr; });
if (in_pool) {
m_max_std_workers--;
}
}
object * wait_any(object * task_list) {