-
Notifications
You must be signed in to change notification settings - Fork 173
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
[Enhancement] generalize tbb's thread_pool_base #1403
Conversation
I would like to propose to rename the boost thread pool to |
ae62c1b
to
fe6f811
Compare
(EDITED) I have a bit of a feeling to adding the standalone ASIO adds a bit of a complexity to the whole architecture and I'm up for reverting if it causes maintenance issues. The source of the complexity:
If you have any better idea to integrate the standalone asio let me know. |
apologies for the delay. i'll get to this soon i hope. |
There were an extra name specifier. It was unnecessary and also tbb specific.
Inorder to use later the thread_pool_base we need to separate it from tbb
The goal is to have a common space for all the thread pools that depends on 3rd party threading libraries, like tbb. The new name is execpools like execution pools, where all thread pools are collected. tbb subdirectory is about separating the different implementation files to manage easily the library dependencies.
The goal is to see that the task_pool_base abstraction is sufficient or not.
The idea is that let's have an asio thread pool rather then a boost. In this way the standalone asio can be also used as an implementation. Which implementation is used for Asio is controlled by the cmake parameter STDEXEC_ASIO_IMPLEMENTATION. It can be 'boost' or 'standalone'. Cmake generates a configuration file that creates a namespace: execpools::asio_impl. This points to the corresponding implementation. In this way the pool implementation is referenced only to this namespace. The configuration file is generated into the build directory, inorder to not pollute the source tree with generated files.
The generated header for asio was located in the build folder, but was included from a public header. It makes the install impossible. Now the file is generated into the include folder in the proper place and added to the gitignore.
c2ca09e
to
a35743a
Compare
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 love this! When I wrote the tbb thread pool (with the help of Eric and others), I factored it like I did with exactly this in mind: That fundamentally an execution context should just need an enqueue
member function and that for the most part everything else will be shared across most thread-pool-like resources.
I left a few comments about my old questions when I wrote it that are still nagging me.
//! This is a P2300-style thread pool wrapping tbb::task_arena, which its docs describe as "A class that represents an | ||
//! explicit, user-managed task scheduler arena." | ||
//! Once set up, a tbb::task_arena has | ||
//! * template<F> void enqueue(F &&f) | ||
//! and | ||
//! * template<F> auto execute(F &&f) -> decltype(f()) | ||
//! | ||
//! See https://spec.oneapi.io/versions/1.0-rev-3/elements/oneTBB/source/task_scheduler/task_arena/task_arena_cls.html |
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.
Seems like my tbb-specific comments should be removed?
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 have to admit it was one of the copy paste errors what I made unfortunately.
Correcting all occurrences.
template <class PoolType, class ReceiverId> | ||
struct operation { | ||
using Receiver = stdexec::__t<ReceiverId>; | ||
struct __t; |
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.
Aside: I keep seeing this pattern, along with stdexec::__t<Foo>
. I see what it's doing, but it's not clear to me why, other than that __t<Foo>
is nicer than typename Foo::__t
. If someone could give an explanation, I'd gladly add a doxygen comment to
template <class _Tp>
using __t = typename _Tp::__t;
in __meta.hpp
.
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.
other than that
__t<Foo>
is nicer thantypename Foo::__t
.
that's it, really. i don't like littering my code with typename
everywhere. not a hill to die on.
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.
Makes sense. With that in mind, why is it seemingly everywhere? Like
class static_thread_pool_ {
template <class ReceiverId>
struct operation {
using Receiver = stdexec::__t<ReceiverId>;
class __t;
};
That's saying that static_thread_pool_
's operation<ReceiverId>
struct has a Receiver
, which it gets from the ReceiverId
, and then has a __t
class which is the operation state? That gets used later to do template <class Receiver> using operation_t = stdexec::__t<operation<stdexec::__id<Receiver>>>;
Why that indirection there? (If you can explain it, I'll add //!
to relevant places!) To me it feels like a round-about way of saying that operation_t<Receiver>
is the operation-state type: You get the __id
of the receiver, get operation<that>
which basically says using Receiver = __t<__id<Receiver>>;
...
I'm sure the indirection is there for a reason, I just don't see it. Is there some more-universal pattern going on that this is just a simple case of?
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.
If I don't misunderstand the question - so feel free to correct me if I do - which is why the indirection is made with the __t
class and what is the reason behind this indirection. Then for me this doc pretty much explains it.
Briefly as I understand: It makes the class type less tight coupled from its template arguments (I.e.: It depends on the Id as argument not on the specific type)
Is it correct @ericniebler? Please correct me when it's wrong.
On the other hand I'm curious whether c++-modules will make changes on it one day or not, but it is just a personal curiosity.
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.
Aah. If there are no objections I’ll add a comment to __t
with breadcrumbs pointing to that doc. Thank you!
return scheduler{static_cast<DerivedPoolType&>(*this)}; | ||
} | ||
|
||
/* Is this even needed? Looks like no. |
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 left this comment in here I think... I didn't get an answer to the question and forgot to remove it.
: pool_(pool) | ||
, rcvr_(std::move(rcvr)) { | ||
this->__execute = | ||
[](task_base* t, std::uint32_t /* tid What is this needed for? */) noexcept { |
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 never got an answer to what the uint32 is for.
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.
When I checked the code I was also wondering. For me it looks like used during bulk tasks this is used to propagate/report the error in the proper into the proper thread. Thus, I thought I'm keeping this comment here, because I'm not 100% sure.
} | ||
|
||
[[nodiscard]] | ||
auto available_parallelism() const -> std::uint32_t { |
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.
This isn't part of P2300 is it? It's just a nice-to-have, I think?
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 couldn't find any reference about it. But from my personal point of view it is nice to have but also I can imagine that not 'all' of the threading libraries has this information or easy to access. I.e.: In asio_thread_pool the _executor
member is required only to fulfill this functional requirement.
Thus, if we removing it then the thread pool will be more generic.
void enqueue(task_base* task, std::uint32_t tid = 0) noexcept { | ||
arena_.enqueue([task, tid] { task->__execute(task, /*tid=*/tid); }); | ||
} |
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.
When I was writing this, it wasn't clear to me where the end of the P2300 concepts are and where things are specialized. I think (?) this pool.enqueue(task)
isn't part of P2300, and is instead an implementation detail of the operation state for the sender for the pool's scheduler.
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 fully agree. I mean I also don't now and I also have the same opinion. Should I add a documentation about it somewhere to the structural description?
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.
we need to leave a forwarding tbbexec/tbb_thread_pool.hpp
header so we don't break people's builds. also, we need to consider that folks are currently linking to the STDEXEC::tbbexec
target. that should continue working.
also, can thread_pool_base.hpp
be used to simplify exec/static_thread_pool.hpp
?
/ok to test |
enqueue(); | ||
} | ||
}; | ||
} // namespace execpools |
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.
A good test/useage-example of this would be like an AnyThreadPool
where tasks are submitted as std::function<void()>
s.
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 agree, more example makes it more usable! I just would like to questioning back, to clarify, if you don't mind.
The idea is to have an AnyThreadPool
that based on the thread_pool_base
but doesn't have a get_scheduler()
public function to schedule tasks on it, but would have an enqueue(std::function<void()> callable)
function how one can submit task to the pool. Is that correct?
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.
Something like that. When I wrote this originally I was coming at it with the notion that a thread-based execution context must be modeled by something g you throw std::function<void()>
s at and that it promises to execute eventually. (Or it could be void(*f)(void*)
s and corresponding void* x
as a pair and it promises to have a thread call f(x)
. That’s weaker and I think still fits.)
Implement review remarks
The goal is to stay compatible with existing usage. Warning flag is added to notify users about the change.
I've pushed some quick update on it:
@ericniebler I think I can see the way how to use the same @BenFrantzDale I'm gonna add as an example the |
no rush, that can be a separate PR. |
/ok to test |
thank you! |
Why?
I would like to use the library with another thread pool than tbb. This PR contains a proposed implementation for this enhancement and solves some issues.
Current Issues
For this purpose the
tbbexec::thread_pool_base
looks like a perfect abstraction. But currently it has two issues:tbb_thread_pool.hpp
, therefore it has a dependency with tbb.friend struct DerivedPoolType::tbb_thread_pool::scheduler;
where thetbb_thread_pool
actually bounding it to the tbb implementation and it is also unnecessary. I.e.:friend struct DerivedPoolType::scheduler;
would be also good.Proposal
My proposal to having a more generic pool hierarchy in the stdexec. Currently it looks like:
I would go with adding 3 implementation for 3rd party thread pools (3 because I like the Rule Of Three):
I choose these 3 3rd-party library, because I think these are the most popular threading libraries.
About the PR
llvm18-cuda12.4
dev-container to test the changes.STDEXEC_ENABLE_BOOST
,STDEXEC_ENABLE_TASK_FLOW
. These should control the dependencies and fetches those when it is necessary.