-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
various improvements in performance, safety, features and readability #27
base: master
Are you sure you want to change the base?
Conversation
Time to leave the past of C with objects and making a difference between these languages by using the appropriated C++ file extension
Merged definitions into class and simplified enqueue return type. Everyone uses editors with folding and lowering compile time is something worth. (Ctor & dtor in class gives the inline hint to compilers as well)
Atomics are cheaper for thread safety
* Pre-allocated workers memory * Simplified creation loop from upcounter to countdown * Build task in-place instead of an additional assignment
* Added this-> to members to make scope more clear * Renamed ctor argument threads -> threads_n (which makes more sense) * Simplified for(;;) to while(true) which is more common and less optimizer work
Why not trying what the OS recommends?
When an equeue happens during the pool destruction, the user did something terribly wrong! Zero threads are possible but result in an useless pool. There aren't plans for dummy pools, right?
Deleted copy&move ctors&assignments because of limitations by attributes (sync objects have nothing of both). Set dtor virtual for polymorphism - ThreadPool has attributes which need to get their dtors called too and running threads shouldn't get lost.
Storing the license in source files too is safer.
I agree the |
|
||
{ | ||
std::unique_lock<std::mutex> lock(this->queue_mutex); | ||
if(!this->stop && this->tasks.empty()) |
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.
Shouldn't this be a while loop? I am getting consistent crashing on line 66 unless I change this to:
while(!this->stop && this->tasks.empty())
This is my (admittedly bizarre) test case:
int main()
{
ThreadPool pool;
for (int i = 0; i < 100000; ++i)
{
pool.enqueue([i]
{
return i;
}).get();
}
return 0;
}
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.
@JoshuaBrookover works for me on VS 2013 with Nov 2013 CTP.
edit: I have a slightly different version to current ThreadPool
class. I prefered the previous version of this class
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.
@h1arshad : @JoshuaBrookover is right. Conditional variables can wake up spuriously and the condition needs to be rechecked on each wakeup. I have modified the code similarly in my fork that I use for log4cplus: https://github.com/log4cplus/ThreadPool/blob/master/ThreadPool.h#L113
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.
It seems to be a bit of a race condition. If I run it enough times (or lower 100000 to something like 100) it will eventually work. But I do wonder why it was changed to begin with, since the original code used the lambda version of condition.wait() (which under the hood uses a while loop):
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
http://en.cppreference.com/w/cpp/thread/condition_variable/wait
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.
@wilx That's true, I wasn't aware what was in the code above. The previous version had such a check.
How come you had two conditional variables in yours (producer and consumer). was it not sufficient just to have one? Or more up front, is there a spurious wake up call that can occur somewhere which i'm not seeing..
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.
@h1arshad: I have two condition variables because there are situation where I want to wake up only producers or only consumers. If I used only one CV, all threads would wake up and the congestion on the mutex would be bigger. All threads would have to wake up to check the condition and some of them would just go to sleep again immediately.
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.
I see! Thanks for the explanation
Date: Thu, 23 Jul 2015 11:16:19 -0700
From: [email protected]
To: [email protected]
CC: [email protected]
Subject: Re: [ThreadPool] various improvements in performance, safety, features and readability (#27)
In ThreadPool.hpp:
- {
if(!threads_n)
throw std::invalid_argument("more than zero threads expected");
this->workers.reserve(threads_n);
for(; threads_n; --threads_n)
this->workers.emplace_back(
[this]
{
while(true)
{
std::function<void()> task;
{
std::unique_lockstd::mutex lock(this->queue_mutex);
if(!this->stop && this->tasks.empty())
@h1arshad: I have two condition variables because there are situation where I want to wake up only producers or only consumers. If I used only one CV, all threads would wake up and the congestion on the mutex would be bigger. All threads would have to wake up to check the condition and some of them would just go to sleep again immediately.
—
Reply to this email directly or view it on GitHub.
By @wilx
From cppreference
Thanks for this hint. I'll delete this commit. |
@wilx I had not seen your comment when I made my second comment. That is good to know about the spurious wakeups, I didn't know it would do that. Thanks! |
@Youka nice change! Instead of several commits, can you squash them all as one change so it's easier to look at. Or create separate pull requests for each of the commits. |
Suggest to declare ThreadPool as final, instead of introducing virtual destructor. |
Interestingly, Chris Kohlhoff's thread_pool implementation (part of a ISO C++ standards proposal) uses https://github.com/chriskohlhoff/executors/blob/master/include/experimental/bits/thread_pool.h#L22 /cc @chriskohlhoff |
Joining in a little late but ...
Sorry but I completely disagree. Love your "role of 5" addition, it's really missing in the original. Also agree with your choice of the ctor default parameter. |
I would be surprised if it doesn't generate the exact same assembly code. And indeed I think it does: https://godbolt.org/g/SLuFaP |
@patrikhuber you are right, it will result in the same executable code. I understood "less optimizer work" to mean that @Youka thought compilation would be faster. I'm pretty sure it won't be (at least, not in a measurable way). |
I tried to keep the original authors style and focused on rather important changes. Everything was tested with the example source but i'd like some impressions too.