Improve work_queue thread pool: correctness fixes, performance, and tests#44
Improve work_queue thread pool: correctness fixes, performance, and tests#44
Conversation
…zations, and tests Correctness: - Catch all exception types (catch(...)) in worker threads, not just std::exception, preventing std::terminate on non-std exceptions - Use if constexpr to conditionally check callable validity, supporting callable types that are not contextually convertible to bool Performance: - Move notify_one() outside lock in submit_task to avoid waking a thread that immediately blocks on the still-held mutex - Remove unnecessary lock re-acquisition for end_task_notifier in worker - Use notify_one instead of notify_all for end_task_notifier since only the destructor waits on it Simplification: - Restructure worker loop: use std::move for single-task extraction instead of splicing into a temporary list - Clean up destructor: remove confusing while/break pattern, use a scoped block to release lock before join - Fix header comment to match filename (YgorThreadPool.h) - Replace unused <algorithm> include with needed <type_traits> Co-authored-by: hdclark <934858+hdclark@users.noreply.github.com> Agent-Logs-Url: https://github.com/hdclark/Ygor/sessions/99a8d908-5ba2-4cd4-9674-5fdc2c8d68ac
Co-authored-by: hdclark <934858+hdclark@users.noreply.github.com> Agent-Logs-Url: https://github.com/hdclark/Ygor/sessions/99a8d908-5ba2-4cd4-9674-5fdc2c8d68ac
There was a problem hiding this comment.
Pull request overview
This PR audits and updates the work_queue thread pool (src/YgorThreadPool.h) to improve correctness and performance, and adds a dedicated tests2 suite to validate behavior and regressions.
Changes:
- Update
work_queueworker loop and shutdown logic (exception handling, task extraction, and condition-variable notification patterns). - Clean up header includes/comment and add
type_traits-based callable validity guarding. - Add
tests2/YgorThreadPool.ccand wire it intotests2/compile_and_run.sh.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/YgorThreadPool.h | Thread pool worker/destructor logic adjustments; exception handling changes; include changes. |
| tests2/YgorThreadPool.cc | New doctest suite covering core thread-pool behaviors (execution, ordering, exception swallowing, clear_tasks, etc.). |
| tests2/compile_and_run.sh | Adds the new thread-pool test translation unit to the explicit tests2 build list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #include <atomic> | ||
| #include <algorithm> | ||
| #include <list> | ||
| #include <type_traits> |
There was a problem hiding this comment.
YgorThreadPool.h uses std::chrono::{nanoseconds,seconds,milliseconds} but does not include <chrono>. The header should be self-contained; relying on indirect includes from <condition_variable> is non-portable. Add #include <chrono> here.
| #include <type_traits> | |
| #include <type_traits> | |
| #include <chrono> |
| while(blocker.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); | ||
| }); | ||
|
|
||
| // Wait for the blocking task to start. | ||
| while(!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
There was a problem hiding this comment.
The clear_tasks subcase uses unbounded busy-wait loops (while(blocker.load()) ... and while(!started.load()) ...). If something regresses (e.g., worker never starts), the test can hang indefinitely and stall CI. Add a timeout/deadline (failing the test if exceeded) or use a condition_variable to wait for started/blocker transitions.
| while(blocker.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| }); | |
| // Wait for the blocking task to start. | |
| while(!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); | |
| auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); | |
| while (blocker.load() && std::chrono::steady_clock::now() < deadline) { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| } | |
| }); | |
| // Wait for the blocking task to start (with timeout to avoid hanging). | |
| auto start_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); | |
| while (!started.load() && std::chrono::steady_clock::now() < start_deadline) { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(1)); | |
| } | |
| REQUIRE(started.load()); |
| }catch(const std::exception &){} | ||
| catch(...){}; | ||
|
|
There was a problem hiding this comment.
There is a stray null statement after the catch(...) block (catch(...){ };). It’s harmless but inconsistent with the preceding catch and makes the exception handling look accidental. Remove the extra ; (and consider formatting the two catch clauses consistently).
| }catch(const std::exception &){} | |
| catch(...){}; | |
| } catch (const std::exception &) { | |
| } catch (...) { | |
| } |
Audit of the
work_queuethread pool insrc/YgorThreadPool.hfor correctness and performance.Correctness
std::exceptionwas caught; a non-std throw (e.g.throw 42;) would invokestd::terminateand crash the process. Addedcatch(...).if(task)validity check withif constexpr— The bareif(user_f)requiresTto be contextually convertible tobool, which fails to compile for lambda types or other callables withoutoperator bool. Now conditional onstd::is_constructible_v<bool, const T &>.Performance
submit_task— Eliminates hurry-up-and-wait where a woken worker immediately blocks on the still-held mutex.end_task_notifierin worker loop —cv::notify_onedoes not require the mutex.notify_oneinstead ofnotify_allforend_task_notifier— only the destructor waits on it.Simplification
std::list+splicewithstd::move(queue.front())+pop_front().while(!l_should_quit){ ... break; }pattern (single-iteration loop) in favor of a scoped lock block.<algorithm>include with<type_traits>.Thread_Pool.h→YgorThreadPool.h).Tests
New
tests2/YgorThreadPool.ccwith subcases for: basic execution, single-worker FIFO ordering,std::exceptionhandling, non-std exception handling,clear_tasks, 1000-task throughput, emptystd::function, default/zero worker count, and recursive task submission from within tasks. All 6683 existing convex hull assertions (the primarywork_queueconsumer) continue to pass.Original prompt
💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.