First time seeing what a thread pool is. Thanks for the content. This is indeed interesting. It also shows why many people just prefer std::async over manually managing std::threads/std::jthreads. It's really hard to get it right.
I am programming in the Windows native thread pool. If you need performance, I think this is true. You can use waiting objects, timers, and Iocp. Therefore, you can use CPU efficiently.
Yes, Thread pools can only be implemented efficiently on System Level. It's crazy that Linux does not offerer this but MacOS/Windows for over 20 years now. Also i miss it a lot in Android.
Nice video and also the visualisation. Also, if you were to use the new concurrency features of C++20, you would greatly simplify your code. I could think of, besides the condition variables, and the locks, using jthread instead and and its cooperative interruption feature (with stop_source and stop_token), binary_semaphore for shutdown or is_empty?, are_we_done?, etc. stuff, and latches|barriers (probably barrier with arrive_and_wait(), if you wanna make that a multi-shot thing). But still, you did the hard job haha. Be safe, Mate.
Yes, I was lazy and didn't want to update the code to C++20. C++23 even has the move only function which would greatly improve the code as well. I will update make a vid about the update.
Good video. A few notes: - why did you use std::bind inside addtask? You can simply stuff the function inside a packaged task (note that the ctor is marked explicit so youhave to do an explicit conversion) - don't wrap a packaged task in a shared pointer, as a packaged tas already is a shared ptr (at leastin libstdc++) - in general your implementation is a good entry level example but it's not very cache friendly and scalability could be better. Soif you want todo some serious lifting you should look into work stealing task pools. On the other hand, this is completely sufficient for a simple game engine or an app.
- About the bind, I don't know, it just happen and I didn't really think about it. - The shared pointer is not necessary for the shared state, but for the type erasure with the lambda function. Since a normal function inside the package task is not a move only type, the capture of the lambda needs to be a copy. To be able to do the copy, I use the shared ptr. In C++23 you can use the move only function, stuff that into the package task and then move it inside the capture of the lambda. That way you don't need the shared ptr. - You are correct. It depends on what you plan to use the thread pool for. The main driver is, how often do you expect your queue to be empty? If often, the current implementation with a shared queue across all threads is the most efficient. If your thread pool is completly busy all the time, I would switch to an implementation with a single queue per thread and implement task stealing. In fact, we use several thread pool implementations based on the task and data at hand.
@@ZenSepiol You don't need type erasue in this case, a packaged task will always accept anything that corresponds to its return value and arguments - which of course in your case you have. But think about it: If you were to just accept only functions that have void return type and zero arguments you could get away with it while at the same time be more transparent. You don't need to do the extra step to stuff the arguments into the functor's fields (using closures), but have the user do it beforehand. This way it is more transparent to the user how the arguments will be used and he will think twice about passing in references that might run out of scope when the caller exits. You also avoid double indirection of operator()() on every loop iteration.
spurious wake ups are not bugs, for example you can get a spurious wakeup when a laptop comes out of sleep mode, there is literally no other way to handle that without re-checking the condition
no need to use bind: template auto AddTask(Fn fn) { auto pTask = std::make_shared(fn); auto f = pTask->get_future(); Add([pTask]() { (*pTask)(); }); return f; }
But then wouldn't you be fixated to no argument (I mean, return_type(void)) functions? I might be wrong, but how can we make it so we could actually send functions with arguments?
First time seeing what a thread pool is. Thanks for the content. This is indeed interesting. It also shows why many people just prefer std::async over manually managing std::threads/std::jthreads. It's really hard to get it right.
I am programming in the Windows native thread pool. If you need performance, I think this is true. You can use waiting objects, timers, and Iocp. Therefore, you can use CPU efficiently.
Yes, Thread pools can only be implemented efficiently on System Level. It's crazy that Linux does not offerer this but MacOS/Windows for over 20 years now. Also i miss it a lot in Android.
Nice video and also the visualisation.
Also, if you were to use the new concurrency features of C++20, you would greatly simplify your code.
I could think of, besides the condition variables, and the locks, using jthread instead and and its cooperative interruption feature (with stop_source and stop_token), binary_semaphore for shutdown or is_empty?, are_we_done?, etc. stuff, and latches|barriers (probably barrier with arrive_and_wait(), if you wanna make that a multi-shot thing).
But still, you did the hard job haha.
Be safe, Mate.
Yes, I was lazy and didn't want to update the code to C++20. C++23 even has the move only function which would greatly improve the code as well.
I will update make a vid about the update.
@@ZenSepiol Very curious watch a vid using the new features
@@ZenSepiol So long as the complex stuff is hid behind a neat interface - who cares? You only need to look at it when it breaks.
Good video. A few notes:
- why did you use std::bind inside addtask? You can simply stuff the function inside a packaged task (note that the ctor is marked explicit so youhave to do an explicit conversion)
- don't wrap a packaged task in a shared pointer, as a packaged tas already is a shared ptr (at leastin libstdc++)
- in general your implementation is a good entry level example but it's not very cache friendly and scalability could be better. Soif you want todo some serious lifting you should look into work stealing task pools. On the other hand, this is completely sufficient for a simple game engine or an app.
- About the bind, I don't know, it just happen and I didn't really think about it.
- The shared pointer is not necessary for the shared state, but for the type erasure with the lambda function. Since a normal function inside the package task is not a move only type, the capture of the lambda needs to be a copy. To be able to do the copy, I use the shared ptr. In C++23 you can use the move only function, stuff that into the package task and then move it inside the capture of the lambda. That way you don't need the shared ptr.
- You are correct. It depends on what you plan to use the thread pool for. The main driver is, how often do you expect your queue to be empty? If often, the current implementation with a shared queue across all threads is the most efficient. If your thread pool is completly busy all the time, I would switch to an implementation with a single queue per thread and implement task stealing. In fact, we use several thread pool implementations based on the task and data at hand.
@@ZenSepiol You don't need type erasue in this case, a packaged task will always accept anything that corresponds to its return value and arguments - which of course in your case you have. But think about it: If you were to just accept only functions that have void return type and zero arguments you could get away with it while at the same time be more transparent. You don't need to do the extra step to stuff the arguments into the functor's fields (using closures), but have the user do it beforehand. This way it is more transparent to the user how the arguments will be used and he will think twice about passing in references that might run out of scope when the caller exits. You also avoid double indirection of operator()() on every loop iteration.
spurious wake ups are not bugs, for example you can get a spurious wakeup when a laptop comes out of sleep mode, there is literally no other way to handle that without re-checking the condition
thanks youu uwuw, i'll need to research templates futures and mutex stuff first & come back.. ;)) #godblessamerica #wtfisakilometar
Also when you get some time can you make a video on thread concurrency, thread exception handler, process mutexing pls
I'll see what I can do! All very good topics to cover.
Can you share information relate to what the matrix panel show 1240 number behind you =))) ?
That was my Subscriber count while making the video 😃
Thank you Zen. Appreciate it. Very elegant code. Can you share the link to the code in your Github pls, i dont see them.
Here you go:
github.com/ZenSepiol/Dockerized-Dear-ImGui/blob/main/src/lib/app_base/thread_pool.hpp
@@ZenSepiol Thank you
no need to use bind:
template
auto AddTask(Fn fn)
{
auto pTask = std::make_shared(fn);
auto f = pTask->get_future();
Add([pTask]() { (*pTask)(); });
return f;
}
But then wouldn't you be fixated to no argument (I mean, return_type(void)) functions?
I might be wrong, but how can we make it so we could actually send functions with arguments?