Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.programming.threads > #836 > unrolled thread

std::thread - abstraction penalty

Started byRani Sharoni <ranisharoni75@gmail.com>
First post2012-03-11 14:13 -0700
Last post2012-03-13 07:27 -0700
Articles 6 — 3 participants

Back to article view | Back to comp.programming.threads


Contents

  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

#836 — std::thread - abstraction penalty

FromRani Sharoni <ranisharoni75@gmail.com>
Date2012-03-11 14:13 -0700
Subjectstd::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]


#837

FromJoshua Maurice <joshuamaurice@gmail.com>
Date2012-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]


#838

FromRani Sharoni <ranisharoni75@gmail.com>
Date2012-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]


#841

FromJoshua Maurice <joshuamaurice@gmail.com>
Date2012-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]


#839

FromUlrich Eckhardt <doomster@knuut.de>
Date2012-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]


#840

FromRani Sharoni <ranisharoni75@gmail.com>
Date2012-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