-
Notifications
You must be signed in to change notification settings - Fork 0
Improve work_queue thread pool: correctness fixes, performance, and tests #44
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,13 @@ | ||||||||||||
| //Thread_Pool.h. | ||||||||||||
| //YgorThreadPool.h. | ||||||||||||
|
|
||||||||||||
| #pragma once | ||||||||||||
|
|
||||||||||||
| #include <mutex> | ||||||||||||
| #include <condition_variable> | ||||||||||||
| #include <thread> | ||||||||||||
| #include <atomic> | ||||||||||||
| #include <algorithm> | ||||||||||||
| #include <list> | ||||||||||||
| #include <type_traits> | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| // Multi-threaded work queue for offloading processing tasks. | ||||||||||||
|
|
@@ -33,7 +33,7 @@ class work_queue { | |||||||||||
| std::unique_lock<std::mutex> lock(this->queue_mutex); | ||||||||||||
|
|
||||||||||||
| // Exercise the condition variables and mutexes, ensuring they are initialized by the implementation. | ||||||||||||
| // This should efectively evaluate to a no-op, but also helps suppress false-positive warning messages in | ||||||||||||
| // This should effectively evaluate to a no-op, but also helps suppress false-positive warning messages in | ||||||||||||
| // Valgrind's DRD tool, i.e., 'not a condition variable', and other tools. | ||||||||||||
| this->new_task_notifier.notify_all(); // No threads waiting, so nothing to notify. | ||||||||||||
| this->end_task_notifier.notify_all(); // No threads waiting, so nothing to notify. | ||||||||||||
|
|
@@ -49,58 +49,54 @@ class work_queue { | |||||||||||
| this->worker_threads.emplace_back( | ||||||||||||
| [this](){ | ||||||||||||
| // Continually check the queue and wait on the condition variable. | ||||||||||||
| bool l_should_quit = false; | ||||||||||||
| while(!l_should_quit){ | ||||||||||||
| while(true){ | ||||||||||||
|
|
||||||||||||
| std::unique_lock<std::mutex> lock(this->queue_mutex); | ||||||||||||
| while( !(l_should_quit = this->should_quit.load()) | ||||||||||||
| while( !this->should_quit.load() | ||||||||||||
| && this->queue.empty() ){ | ||||||||||||
|
|
||||||||||||
| // Waiting releases the lock, which allows for work to be submitted. | ||||||||||||
| // | ||||||||||||
| // Note: spurious notifications are OK, since the queue will be empty and the worker will return to | ||||||||||||
| // waiting on the condition variable. | ||||||||||||
| // this->new_task_notifier.wait(lock); | ||||||||||||
| this->new_task_notifier.wait_for(lock, std::chrono::seconds(2) ); // No notifiers, so no signal to receive. | ||||||||||||
| this->new_task_notifier.wait_for(lock, std::chrono::seconds(2) ); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if(this->queue.empty()){ | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Assume ownership of only the first item in the queue (FIFO). | ||||||||||||
| std::list<T> l_queue; | ||||||||||||
| if(!this->queue.empty()) l_queue.splice( std::end(l_queue), this->queue, std::begin(this->queue) ); | ||||||||||||
|
|
||||||||||||
| //// Assume ownership of all available items in the queue. | ||||||||||||
| //std::list<T> l_queue; | ||||||||||||
| //l_queue.swap( this->queue ); | ||||||||||||
|
|
||||||||||||
| // Perform the work in FIFO order. | ||||||||||||
| auto task = std::move(this->queue.front()); | ||||||||||||
| this->queue.pop_front(); | ||||||||||||
|
|
||||||||||||
| lock.unlock(); | ||||||||||||
| for(const auto &user_f : l_queue){ | ||||||||||||
| try{ | ||||||||||||
| if(user_f){ | ||||||||||||
| user_f(); | ||||||||||||
| } | ||||||||||||
| }catch(const std::exception &){}; | ||||||||||||
|
|
||||||||||||
| lock.lock(); | ||||||||||||
| this->end_task_notifier.notify_all(); | ||||||||||||
| lock.unlock(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Perform the work. | ||||||||||||
| try{ | ||||||||||||
| if constexpr(std::is_constructible_v<bool, const T &>){ | ||||||||||||
| if(task) task(); | ||||||||||||
| }else{ | ||||||||||||
| task(); | ||||||||||||
| } | ||||||||||||
| }catch(const std::exception &){} | ||||||||||||
| catch(...){}; | ||||||||||||
|
|
||||||||||||
|
Comment on lines
+82
to
+84
|
||||||||||||
| }catch(const std::exception &){} | |
| catch(...){}; | |
| } catch (const std::exception &) { | |
| } catch (...) { | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||||||||
| #include <atomic> | ||||||||||||||||||||||||||||||||||||
| #include <chrono> | ||||||||||||||||||||||||||||||||||||
| #include <functional> | ||||||||||||||||||||||||||||||||||||
| #include <stdexcept> | ||||||||||||||||||||||||||||||||||||
| #include <thread> | ||||||||||||||||||||||||||||||||||||
| #include <vector> | ||||||||||||||||||||||||||||||||||||
| #include <mutex> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| #include <YgorThreadPool.h> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| #include "doctest/doctest.h" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| TEST_CASE( "work_queue" ){ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| SUBCASE("basic task execution"){ | ||||||||||||||||||||||||||||||||||||
| std::atomic<int> counter{0}; | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| work_queue<std::function<void()>> pool(2); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&counter](){ ++counter; }); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&counter](){ ++counter; }); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&counter](){ ++counter; }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| REQUIRE(counter.load() == 3); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| SUBCASE("single worker sequential FIFO order"){ | ||||||||||||||||||||||||||||||||||||
| std::vector<int> results; | ||||||||||||||||||||||||||||||||||||
| std::mutex results_mutex; | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| work_queue<std::function<void()>> pool(1); | ||||||||||||||||||||||||||||||||||||
| for(int i = 0; i < 10; ++i){ | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([i, &results, &results_mutex](){ | ||||||||||||||||||||||||||||||||||||
| std::lock_guard<std::mutex> lock(results_mutex); | ||||||||||||||||||||||||||||||||||||
| results.push_back(i); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| REQUIRE(results.size() == 10); | ||||||||||||||||||||||||||||||||||||
| for(int i = 0; i < 10; ++i){ | ||||||||||||||||||||||||||||||||||||
| REQUIRE(results[static_cast<size_t>(i)] == i); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| SUBCASE("handles std::exception without crashing"){ | ||||||||||||||||||||||||||||||||||||
| std::atomic<int> counter{0}; | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| work_queue<std::function<void()>> pool(2); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([](){ throw std::runtime_error("test"); }); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&counter](){ ++counter; }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| REQUIRE(counter.load() == 1); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| SUBCASE("handles non-std exceptions without crashing"){ | ||||||||||||||||||||||||||||||||||||
| std::atomic<int> counter{0}; | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| work_queue<std::function<void()>> pool(2); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([](){ throw 42; }); | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&counter](){ ++counter; }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| REQUIRE(counter.load() == 1); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| SUBCASE("clear_tasks removes pending tasks"){ | ||||||||||||||||||||||||||||||||||||
| std::atomic<int> counter{0}; | ||||||||||||||||||||||||||||||||||||
| work_queue<std::function<void()>> pool(1); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Submit a blocking task to hold the single worker. | ||||||||||||||||||||||||||||||||||||
| std::atomic<bool> blocker{true}; | ||||||||||||||||||||||||||||||||||||
| std::atomic<bool> started{false}; | ||||||||||||||||||||||||||||||||||||
| pool.submit_task([&blocker, &started](){ | ||||||||||||||||||||||||||||||||||||
| started.store(true); | ||||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+74
to
+78
|
||||||||||||||||||||||||||||||||||||
| 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()); |
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.
YgorThreadPool.husesstd::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.