Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.programming.threads > #836 > unrolled thread
| Started by | Rani Sharoni <ranisharoni75@gmail.com> |
|---|---|
| First post | 2012-03-11 14:13 -0700 |
| Last post | 2012-03-13 07:27 -0700 |
| Articles | 6 — 3 participants |
Back to article view | Back to comp.programming.threads
std::thread - abstraction penalty Rani Sharoni <ranisharoni75@gmail.com> - 2012-03-11 14:13 -0700
Re: std::thread - abstraction penalty Joshua Maurice <joshuamaurice@gmail.com> - 2012-03-12 15:27 -0700
Re: std::thread - abstraction penalty Rani Sharoni <ranisharoni75@gmail.com> - 2012-03-13 01:43 -0700
Re: std::thread - abstraction penalty Joshua Maurice <joshuamaurice@gmail.com> - 2012-03-13 15:07 -0700
Re: std::thread - abstraction penalty Ulrich Eckhardt <doomster@knuut.de> - 2012-03-13 09:53 +0100
Re: std::thread - abstraction penalty Rani Sharoni <ranisharoni75@gmail.com> - 2012-03-13 07:27 -0700
| From | Rani Sharoni <ranisharoni75@gmail.com> |
|---|---|
| Date | 2012-03-11 14:13 -0700 |
| Subject | std::thread - abstraction penalty |
| Message-ID | <6cd50e44-6888-4f0a-94fa-a736b0d877da@k24g2000yqe.googlegroups.com> |
Hi, I recently encountered std::thread implementation in which the caller thread waits for target threads to start and evaluate arguments before continuing. This seems to create a sub-optimal serialization due to potential use cases that might rarely be used (e.g. TLS sensitive copying and custom transfer of ownership). For example, spinning 3 threads yields: Create-thread-1, wait-1, Create-thread-2, wait-2, Create-thread-3, wait-3, continue The requirements is per C++ standard 30.3.1.2/4 (thread construction): The *new thread of execution executes* INVOKE(DECAY_-COPY ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) with the calls to DECAY_COPY being evaluated in the constructing thread. There might be some option to optimize this serialization but I'm not sure that any implementation will do so. What do you think about such abstraction penalty in general purpose standard facility? I'm quite sure that I noticed something similar in the .NET run-time. Thanks, Rani
[toc] | [next] | [standalone]
| From | Joshua Maurice <joshuamaurice@gmail.com> |
|---|---|
| Date | 2012-03-12 15:27 -0700 |
| Message-ID | <d3225b8b-8db8-4996-abdb-1b6e0fc45ac9@x10g2000pbi.googlegroups.com> |
| In reply to | #836 |
On Mar 11, 2:13 pm, Rani Sharoni <ranisharon...@gmail.com> wrote:
> Hi,
>
> I recently encountered std::thread implementation in which the caller
> thread waits for target threads to start and evaluate arguments before
> continuing. This seems to create a sub-optimal serialization due to
> potential use cases that might rarely be used (e.g. TLS sensitive
> copying and custom transfer of ownership).
Could you explain this more please? I am curious.
> For example, spinning 3 threads yields:
> Create-thread-1, wait-1, Create-thread-2, wait-2, Create-thread-3,
> wait-3, continue
>
> The requirements is per C++ standard 30.3.1.2/4 (thread construction):
> The *new thread of execution executes* INVOKE(DECAY_-COPY
> ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) with
> the calls to DECAY_COPY being evaluated in the constructing thread.
>
> There might be some option to optimize this serialization but I'm not
> sure that any implementation will do so.
> What do you think about such abstraction penalty in general purpose
> standard facility?
> I'm quite sure that I noticed something similar in the .NET run-time.
I'd like to understand the use cases and how the code solves it. I'm
sorry that I don't quite follow.
I'm looking at my own thread abstraction right now, and it seems that
I:
1- In the user thread: allocate an object with new for the runnable/
arguments
2- In the user thread: create and start new thread with _beginthreadex
or pthread_create
3- In the new thread: call the user's runnable
4- In the new thread: free the allocated object with delete.
With that, I don't need to do any explicit waiting with condition
variables, nor semaphores, etc.
Here's a copy of the relevant code (slightly reformated):
// ** header
//Runnable is an empty class with a virtual run() method.
inline JThread::JThread(Runnable* runnable)
: state(joinable)
{ start(runnable);
}
// callable_t must have operator() defined, or this will be a compile
error
template <typename callable_t>
inline JThread::JThread(callable_t callable)
: state(joinable)
{ // RunnableTemplate is a subclass of Runnable which defines run()
to call 'callable()'
start(new RunnableTemplate<callable_t>(callable));
}
// ** cpp
#ifdef _WIN32
extern "C" unsigned int __stdcall
jjEntryPointForThreadsWin32(void* x)
#else
extern "C" void* jjEntryPointForThreadsPosix(void* x)
#endif
{ std::auto_ptr<JThread::Runnable>
runnable(static_cast<JThread::Runnable*>(x));
runnable->run();
return 0;
}
void JThread::start(Runnable* runnable)
{
auto_ptr<Runnable> runObject(runnable);
#ifdef _WIN32
uintptr_t const tmpThreadHandle = _beginthreadex(
0, //default security attributes
0, //default stack size
& jjEntryPointForThreadsWin32,
runObject.get(),
0, //make it start running, alternative is to create thread in
suspended state
0); //out-param for threadid
threadHandle = reinterpret_cast<HANDLE>(tmpThreadHandle);
if (0 == threadHandle)
FATAL(0, 0);
#else
int x = pthread_create(
& thread,
0,
& jjEntryPointForThreadsPosix,
runObject.get());
if (x)
FATAL(x, 0);
#endif
runObject.release();
}
[toc] | [prev] | [next] | [standalone]
| From | Rani Sharoni <ranisharoni75@gmail.com> |
|---|---|
| Date | 2012-03-13 01:43 -0700 |
| Message-ID | <25394692-1ea1-483c-a5be-e976aa336d9a@9g2000pbn.googlegroups.com> |
| In reply to | #837 |
On Mar 13, 12:27 am, Joshua Maurice <joshuamaur...@gmail.com> wrote:
> On Mar 11, 2:13 pm, Rani Sharoni <ranisharon...@gmail.com> wrote:
>
> > I recently encountered std::thread implementation in which the caller
> > thread waits for target threads to start and evaluate arguments before
> > continuing. This seems to create a sub-optimal serialization due to
> > potential use cases that might rarely be used (e.g. TLS sensitive
> > copying and custom transfer of ownership).
>
> Could you explain this more please? I am curious.
Sure - consider the following requirements:
> > The requirements is per C++ standard 30.3.1.2/4 (thread construction):
> > The *new thread of execution executes* INVOKE(DECAY_-COPY
> > ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) with
> > the calls to DECAY_COPY being evaluated in the constructing thread.
And the following code:
struct A {};
void func(A&) {}
void f()
{
A a = {};
std::thread(func, a);
}
According to the above requirement (invoke decay copying) the copying
of 'a' should be done in the *new thread* and since this is a local
variable the std::thread caller must wait or otherwise 'a' will go out
of scope...
Rani
[toc] | [prev] | [next] | [standalone]
| From | Joshua Maurice <joshuamaurice@gmail.com> |
|---|---|
| Date | 2012-03-13 15:07 -0700 |
| Message-ID | <f19dedaf-6cf1-43c4-897f-687083d4b6ca@w32g2000vbt.googlegroups.com> |
| In reply to | #838 |
On Mar 13, 1:43 am, Rani Sharoni <ranisharon...@gmail.com> wrote:
> On Mar 13, 12:27 am, Joshua Maurice <joshuamaur...@gmail.com> wrote:
>
> > On Mar 11, 2:13 pm, Rani Sharoni <ranisharon...@gmail.com> wrote:
>
> > > I recently encountered std::thread implementation in which the caller
> > > thread waits for target threads to start and evaluate arguments before
> > > continuing. This seems to create a sub-optimal serialization due to
> > > potential use cases that might rarely be used (e.g. TLS sensitive
> > > copying and custom transfer of ownership).
>
> > Could you explain this more please? I am curious.
>
> Sure - consider the following requirements:
>
> > > The requirements is per C++ standard 30.3.1.2/4 (thread construction):
> > > The *new thread of execution executes* INVOKE(DECAY_-COPY
> > > ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) with
> > > the calls to DECAY_COPY being evaluated in the constructing thread.
>
> And the following code:
>
> struct A {};
> void func(A&) {}
>
> void f()
> {
> A a = {};
> std::thread(func, a);
>
> }
>
> According to the above requirement (invoke decay copying) the copying
> of 'a' should be done in the *new thread* and since this is a local
> variable the std::thread caller must wait or otherwise 'a' will go out
> of scope...
Ah, thank you very much. Makes perfect sense.
[toc] | [prev] | [next] | [standalone]
| From | Ulrich Eckhardt <doomster@knuut.de> |
|---|---|
| Date | 2012-03-13 09:53 +0100 |
| Message-ID | <9s8g7fF18hU1@mid.uni-berlin.de> |
| In reply to | #836 |
Rani Sharoni wrote: > I recently encountered std::thread implementation in which the caller > thread waits for target threads to start and evaluate arguments before > continuing. This seems to create a sub-optimal serialization due to > potential use cases that might rarely be used (e.g. TLS sensitive > copying and custom transfer of ownership). I beg to differ here: Transferring ownership is not uncommon. Actually, I find it very common that a thread operates on some context object and that it either exclusively owns that object or shares ownership. The shared ownership case (shared_ptr) is easily solved, because you just copy the smart pointer object, the caller can't tell the difference anyway. However, in order to get the exclusive ownership case right, you pass an auto_ptr [1] to the function starting the thread. If this function fails to start the thread you should retain ownership of the resource [2], so it can't blindly copy the smart pointer. Instead, it must provide the smart pointer as a reference to the thread startup function, but then it has to wait until that function has "picked up" the resource from that reference. One thing that contradicts this theory is the fact that the "DECAY-COPY" operations are running in the constructing thread and I would expect these to already transfer ownership. But maybe I'm reading something into those statements that isn't there, I don't have the full text at hand. Note that I'm only guessing the rationale for this requirement, which indeed penalizes the startup latency. In most cases, the C++ Committee is not blind though and also concerned with speed, so I wouldn't rule out ways to get around this. One thing I would look at are thread pools, I know that at least Boost.Thread has an according class. Greetings! Uli [1] I'm aware of unique_ptr and that auto_ptr is deprecated, but I'm not 100% sure about its capabilities and behaviour. It is also intended for exclusive resource ownership though, so I guess it will be comparable. [2] You could argue differently, i.e. that once you try to start the thread you yield any claims to the resource. I guess this option was rejected, because it might do surprising things in the rare case that thread startup fails, which would then probably catch people unaware.
[toc] | [prev] | [next] | [standalone]
| From | Rani Sharoni <ranisharoni75@gmail.com> |
|---|---|
| Date | 2012-03-13 07:27 -0700 |
| Message-ID | <cb86e0d6-c1cc-4095-b1cf-4f52ff7c2d75@or10g2000pbc.googlegroups.com> |
| In reply to | #839 |
On Mar 13, 10:53 am, Ulrich Eckhardt <dooms...@knuut.de> wrote: > Rani Sharoni wrote: > > I recently encountered std::thread implementation in which the caller > > thread waits for target threads to start and evaluate arguments before > > continuing. This seems to create a sub-optimal serialization due to > > potential use cases that might rarely be used (e.g. TLS sensitive > > copying and custom transfer of ownership). > > However, in order to get the exclusive ownership case right, you pass an > auto_ptr [1] to the function starting the thread. If this function fails > to start the thread you should retain ownership of the resource [2], so it > can't blindly copy the smart pointer. Instead, it must provide the smart > pointer as a reference to the thread startup function, but then it has to > wait until that function has "picked up" the resource from that reference. Exclusive transfer of ownership that requires wait for the target thread to start is quite rare in the native threads worlds. The common case is to pass a context and if create-thread succeeded then transfer of ownership is guaranteed. NO need to wait. Note that according to the standard, the caller is not invoking copying in his thread so even for the share_ptr<> case the caller has to wait for the target thread to take a reference (i.e. invoke share_ptr copy). Passing unique_ptr is tricky since it doesn't support copy. One will probably have to write a custom class to "move" such objects into the target thread but this is really overkill compared with common practices with native threads. Note that if you copy multiple objects (e.g. std::string and custom moveable object) then there is no strong guarantee anyway since some copy ctors might throw after transfer of ownership related to other objects. > Note that I'm only guessing the rationale for this requirement, which > indeed penalizes the startup latency. In most cases, the C++ Committee is > not blind though and also concerned with speed, so I wouldn't rule out > ways to get around this. Hence my surprise. I guess that they inherited this from boost. std::thread is the only abstraction for thread creation and I can only guess that most implementation will perform the serialization even for cases that can be optimized (e.g. passing c-style function and void* arg). Rani
[toc] | [prev] | [standalone]
Back to top | Article view | comp.programming.threads
csiph-web