Intel® oneAPI Threading Building Blocks
Ask questions and share information about adding parallelism to your applications when using this threading library.

Body concept question

renorm
Beginner
932 Views
By Body I mean function objetcs passed to parallel algorithms such as parallel_for. The reference manual tells us that the call operator must be const. But objects are passed by const reference and copied into each thread. There is no really a need to make the call operator const. Task's execute method doesn't have to be const.

Just curious,
renorm.
0 Kudos
19 Replies
RafSchietekat
Valued Contributor III
932 Views
Having const with a Body helps to avoid mistakes, because otherwise the state would depend on the Body's unpredictable ancestry and perhaps reuse history (depending on the algorithm), unless state changes are intentional and carefully controlled (e.g., in parallel_reduce). A task is constructed and often recycled entirely at user discretion, so it makes sense to not prevent/discourage state changes prior to recycling.
0 Kudos
smasherprog
Beginner
932 Views
You are right, the const does not NEED to be there; however, since tbb is a library for others to use, it DOES need to be there.

When you start programming, private, and protect in classes are not needed. Why? Becuase you are the only one in your code, but if you start selling your code or have thousands of other people using it, private, protected and const get used ALOT in order to tell the users about the intended functionality to ensure they dont break the code.
0 Kudos
renorm
Beginner
932 Views
But objects with immutable state are less flexible. If an object is designed to do nontrivial computations and produce useful data, then it needs to do its work incrementally, step by step. Incremental computation requires mutable state to save intermediate results. const methods can't use TLS members, because TLS's local() method is non-const. In multithreaded world only re-entrant methods should be declared const. Using mutable keyword on data members and casting away constness of this pointer is a minefield. Ever since I started working with threads I stopped trusting others const, because I don't believe there is a way to make every useful method re-entrant. If I could make all my methods re-entrant, why would I need TBB?

0 Kudos
smasherprog
Beginner
932 Views
Well, in the paralell_for example you mentioned, if the function was not const and a user changed the internal state of the class, it could break and go crash boom!. If you need the function to change something, the do it, just dont mess with the this pointer.

Since there isnt a need to make it non-const, the designers of tbb, made it const so the users can understand: dont change the class! Does this limit you im some way? I am confused as to why you are even concerned with something like this.

Ok, so, you dont like the const, why? What is it that you are trying to accomplish that the const is not allowing?

I think your concern is misplaced due to the fact that you have not delved far enough into tbb yet.

Remeber, its only the this pointer that is const, you can change anything else you want. :)
0 Kudos
renorm
Beginner
932 Views
parallel_for copies its task object. Nothing will go boom unless the copies share non-synchronized mutable variables.

Tully const methods are much less flexible. The const requiremnt placed on Body is incongrous with non-const execute() method of task class. Currently, one either has to use mutable keyword, const_cast on this pointer or move all mutable variables outside (make them global?). All these encourages bad coding practice.

Boost threads don't require const call method for a good reason. Usual practice is to copy mutable variables into each thread. const requirement is redundant.
0 Kudos
renorm
Beginner
932 Views
Having const with a Body helps to avoid mistakes, because otherwise the state would depend on the Body's unpredictable ancestry and perhaps reuse history.

Anyone using such a Body is asking for trouble and he deserves it. Copy/reuse with side effects is too bad even in single threaded world. But there are plenty of uses for non-const Body. Non-trivial method can't be made const, except in rare cases. So, what are the alternatives? Either use task scheduler explicitly or defeat const with mutable or const_cast. Explicit task creation is more complex and declaring non-re-entrant methods as const is a minefield.
0 Kudos
RafSchietekat
Valued Contributor III
932 Views

#3 "const methods can't use TLS members, because TLS's local() method is non-const."
That seems all right to me (unless there would be other issues elsewhere): if you want a TLS object, you should have a reference to a unique control instance, not a member variable per Body, because each of those controls a separate thread-to-instance map, which isn't very useful (and quite expensive!). The const doesn't apply to the referent, and you're free to call local().

#3 "Using mutable keyword on data members and casting away constness of this pointer is a minefield."
Technically you could probably use those because there is no shared access (I'd have to check to be certain), but you don't need to go there.

#4 "Well, in the paralell_for example you mentioned, if the function was not const and a user changed the internal state of the class, it could break and go crash boom!"
You don't have to invent reasons. :-)

#5 "The const requiremnt placed on Body is incongrous with non-const execute() method of task class."
See #1.

#5 "Currently, one either has to use mutable keyword, const_cast on this pointer or move all mutable variables outside (make them global?). All these encourages bad coding practice."
What is the purpose of changing the Body? In parallel_for, most Body instances are copies, so if you want to build incremental knowledge, you are better off with TLS, and otherwise you might as well make another copy. You could also use parallel_reduce instead.

#5 "Boost threads don't require const call method for a good reason. Usual practice is to copy mutable variables into each thread. const requirement is redundant."
There are orders of magnitude more tasks than threads, so the concerns are different.

#6 "Anyone using such a Body is asking for trouble and he deserves it. Copy/reuse with side effects is too bad even in single threaded world."
This is not about side effects, but about having many Body instances that relate unpredictably to each other. Maybe you should give an example for our consideration, so that we know what you want to do, specifically. Who knows, maybe const is just helpfully preventing you from making a design mistake?

0 Kudos
renorm
Beginner
932 Views
This pseudo code should illustrate my point better (hopefully).
[cpp]class Body {
public:
    Body(const Exemplar& exemplar)
    : tls(new enumerable_thread_specific >(exemplar)) {}

    // default copy constructor is OK
    // default assigment is OK

    void operator()(const blocked_range& range) {
        // reference to thread local copy
        ptr_to_local_copy = &tls->local();

        // initialize tmp and rng from range
        [...]

        non_const_method();
    }

private:
    void non_const_method() { /*mutating method*/ };

    shared_ptr > tls;
    Exemplar* ptr_to_local_copy;
    vector tmp;
    pseudorandom_number_generator_type rng;

};[/cpp]

The call operator can't be const for the following reasons:

tmp stores intermediate results. Each copy of Body has its own tmp. tmp is not TLS, because it is initialized from blocked_range.

pseudorandom_number_generator_type
must be lazily initialized.

ptr_to_local_copy
can't be initialized by the constructor.

The call operator calls non_const_method().
0 Kudos
RafSchietekat
Valued Contributor III
932 Views
Why not define "tls" without the shared_ptr outside Body on the stack before calling parallel_for, and pass around a pointer or reference...

An object that really costs a lot to initialise, like the random-number generator maybe, shouldn't be instantiated in every Body, unless it would be that only a small fraction of them uses one. Perhaps it belongs in TLS instead?

The issue seems to be that you want to communicate between operator() and non_const_method() using member variables, but isn't that why somebody invented function parameters? And if you call a separate computation object from a small loop kernel, perhaps a lambda, you wouldn't (or shouldn't) even think of it as a workaround: a Body is just too ephemeral to usefully hold any mutable state.
0 Kudos
Alexey-Kukanov
Employee
932 Views
Here is how the question is answered in the Tutorial:

Because the body object might be copied, its operator() should not modify the body. Otherwise the modification might or might not become visible to the thread that invoked parallel_for, depending upon whether operator() is acting on the original or a copy. As a reminder of this nuance, parallel_for requires that the body object's operator() be declared const.

In other words, constness of operator(), the only public method ofBody, serves as the way to "document" the contract: one should not expect that the Body object passed to parallel_for can be used as an accumulator to hold the results of the loop calculations. And in fact, parallel_for's body is expected to be a lightweight, easy-to-copyclosure, not something that carries a lot of business logic.

As you mentioned, there is a plenty of ways to overcome this; and some are "inelegant" more than others. I think that clear separation of concepts is the best possible solution here: encapsulate the logic into a special class, and reference an instance of that class from the body object, or maybe even create it as an automatic variable inside the operator(). My second preference would be to use parameters to pass the information between internal methods (as suggested by Raf); and if for some variables it wouldseeminconvenient, I would not mind using mutable class members, especially if those were just a few. I would avoid const_cast becauseit would look like an ugly hack, and arguably it would *be* exactly that.

0 Kudos
renorm
Beginner
932 Views
Defining TLS outside Body exposes implementation details to the user. There can be many groups of instances of Body, each group with its own TLS. Holding TLS by a pointers inside Body hides it from the user and makes copying cheap. TLS must be destroyed once all instances of Body from the same group are destroyed. Random-number generator can be moved into TLS, but ptr_to_local_copy can't be const, unless TLS is moved outside Body. Sometimes using member variables is preferable to using many function parameters, especially with recursive functions.

If all copies of Body are made from the original exemplar, then the call operators doesn't need to be const. Given that someone might use Body with mutable class members (in fact, many C++ programmers do that often), all copies should be made from the original exemplar anyway. Making the call operator const doesn't protect users from possible mistakes, because const can be easily bypassed. Const forces users to use inelegant hacks (like mutable members) or keep mutable variables outside Body. More admin and more complex bookkeeping means more room for errors. The only thing Body needs is a copy constructor without side effects. In multithreaded world, const must mean the same thing as re-entrant. Re-entrant is too restrictive. Body doesn't need to be re-entrant. Yes, Body must be cheap to copy, but it a different issue.

Btw, STL has the same problem. In theory, STL algorithms can do the following with the passed function object:
1) Copy it undefined number of times.
2) Copy, use, then copy again, then use again.

In practice, no implementation does that, except maybe copying the object once at the very beginning.
0 Kudos
RafSchietekat
Valued Contributor III
932 Views
I get the feeling you will not be convinced about separation of concerns. But could you then perhaps accept the required use of mutable as a form of self-documentation?

"Holding TLS by a pointers inside Body hides it from the user and makes copying cheap."
Reference-counted smart pointers have scalability issues, on some processors even more than on others, which is why I advised against their use as a first choice. But I'm unsure as to how much, and you say you are obliged to use them, so...

"In multithreaded world, const must mean the same thing as re-entrant."
How do you figure that?

"Btw, STL has the same problem."
I would say that STL has the same design. Sometimes less is more. :-)

0 Kudos
renorm
Beginner
932 Views
I entirely understand why cont is there, but I feel that it is not worth the trouble. TBB developers can't assume that Body's call operator is actually const, because people are free to use mutable members. TBB must be implemented as if the call operator isn't const. Each instance must be copied from the original exemplar and each copy is not reused once its call operator has returned. If it is not true, then Body must be truly const (no mutables allowed).

In multithreaded world declaring mutating methods as const is a minefield. When the users want to use non-const method they either synchronize the access to it or copy the object into each thread. But if the method is declared as const, anyone is free to assume that no synchronization is need. Re-entrant const methods can be very useful, but mutating const members are minefields.

For example it is very helpful to know that std::vector::size is re-entrant. But Body which uses rand() from CRT is not re-entrant, even if it has no data fields at all. Therefore, the users must ensure that the call operator doesn't use non-re-entrant shared resources.
0 Kudos
RafSchietekat
Valued Contributor III
932 Views
"I entirely understand why cont is there, but I feel that it is not worth the trouble."
Let's agree to disagree.

"TBB developers can't assume that Body's call operator is actually const, because people are free to use mutable members."
TBB does seem to happen to throw away each Body after use, but that's not the toolkit's concern, really. Whatever happened to "Anyone using such a Body is asking for trouble and he deserves it."?

"But if the method is declared as const, anyone is free to assume that no synchronization is need."
That is wishful thinking: nothing currently part of C++ can promise thread-safety (which is orthogonal to constness), only the documentation can.

"For example it is very helpful to know that std::vector::size is re-entrant."
I assume that you are using reentrant as a synonym for thread-safe (unless I missed something, a member function can be reentrant in the sense that it can concurrently be invoked on different objects, but still not thread-safe when concurrently invoked on the same object)? In this case, like for index evaluation, thread-safety seems like a safe assumption, but I think that the standard is negligent by not documenting any of that yet, last time I looked anyway.

0 Kudos
renorm
Beginner
932 Views
Yeh, C++ standard knows nothing about threads, yet.

A member function is reentrant if multiple threads can invoke it on the same object. Invoking a member function on different copies is thread safe by default, unless the instances share some mutable data (certainly, "Anyone using such a Body is asking for trouble and he deserves it.")

TBB doesn't need reentrant Body, it needs thread safe copies of Body.

const is synonym to immutable. Successive calls on the same object should give the same result. TBB invokes the call operator on each copy of Body only once. const doesn't really apply here.

With truly non-mutating call operator each thread could reuse its copy of Body by letting it to process more then one section of blocked range. But there is no way to enforce the call operator as const.
0 Kudos
RafSchietekat
Valued Contributor III
932 Views

"Yeh, C++ standard knows nothing about threads, yet."
I meant including the new version (in progress), up until N3000 at least, unless I missed anything.

"A member function is reentrant if multiple threads can invoke it on the same object. Invoking a member function on different copies is thread safe by default, unless the instances share some mutable data (certainly, "Anyone using such a Body is asking for trouble and he deserves it.")"
Don't take my word for it, e.g., see http://doc.qt.nokia.com/4.6/threads-reentrancy.html.

"TBB doesn't need reentrant Body, it needs thread safe copies of Body."
Reentrant should be enough, as all threads operate on different instances.

"const is synonym to immutable. Successive calls on the same object should give the same result. TBB invokes the call operator on each copy of Body only once. const doesn't really apply here."
const is by no means synonymous with immutable. In C++ it's a contract that if you get a const reference you are not allowed to significantly modify the object (beyond what has been declared mutable), but other code holding a non-const reference can still modify it when your back is turned (concurrently or otherwise). Only the author can make an object immutable, by not providing any way of modifying it even to code that has a non-const reference.

"With truly non-mutating call operator each thread could reuse its copy of Body by letting it to process more then one section of blocked range. But there is no way to enforce the call operator as const."
C++ can be subverted in many ways, so to be productive you have to work with it, not against it: mutable should be used only for part of the state that doesn't significantly change the object, such as a cached value.

0 Kudos
renorm
Beginner
932 Views
Oh I see. I was using thread safe and reentrant as the same thing. Yes, according to that QT manual TBB needs only reentrant Body. But where const fits here? Passing Body by const is one thing but forcing the call operator as const is something different. parallel_for takes an instance of Body by const reference, which as you said, is a contract not to modified it. But const on the method is a different type of contract. The call operator doesn't need to be const unless TBB is intended to use the passed instance directly. TBB requires a const call operator, but TBB can't take advantages of the constness.

With truly const call operator, e.a. successive calls from the same thread give the same result, each thread could reuse its copy of Body. Each thread gets exactly one copy and reuses it until all work is done. But TBB can't make that assumption, because that would be a minefield.
0 Kudos
RafSchietekat
Valued Contributor III
932 Views
"But where const fits here? Passing Body by const is one thing but forcing the call operator as const is something different. parallel_for takes an instance of Body by const reference, which as you said, is a contract not to modified it. But const on the method is a different type of contract. The call operator doesn't need to be const unless TBB is intended to use the passed instance directly. TBB requires a const call operator, but TBB can't take advantages of the constness."
I think we've been here already. Like STL, TBB calls with const by design: less is more. If you want to modify the object anyway, explicitly use mutable, or separate concerns by using another object to pass parameters as member variables, or put those parameters together in a struct that's allocated as a local variable in operator() and then passed around by reference as a single argument, or any other means, but in general code quality benefits from a discipline that doesn't modify the function object.

"With truly const call operator, e.a. successive calls from the same thread give the same result, each thread could reuse its copy of Body. Each thread gets exactly one copy and reuses it until all work is done. But TBB can't make that assumption, because that would be a minefield."
Not significantly modifying Body in a const operation is your responsibility, and making those pseudo-parameters mutable would not pose a problem. Perhaps somebody else could comment on the relative efficiency of parallel_for vs. parallel_reduce, which does reuse Body instances (for functional reasons), or you could speculate that there is no difference and benchmark them just to prove a point (and potentially have an additional workaround). :-) Intuitively, and since parallel_for is not simply implemented on top of parallel_reduce, I doubt it somewhat, but it would be interesting to have some figures.

Unless there are new issues, let's leave it at that.

(Added and partially retracted) Oh yes, on poor old computers without parallelism, it would be better to avoid all manner of parallelism overhead, so it wouldn't do to make a throwaway copy each time. But that's just an additional argument, and not very strong.
0 Kudos
renorm
Beginner
932 Views
Let's leave it at that. Thanks for very informative discussion.
0 Kudos
Reply