Boost logo

Boost :

From: Malcolm Noyes (boost_at_[hidden])
Date: 2005-05-12 18:27:21


> Although Boost.Threads uses templates in expressing
>some of its capabilities, it is a relatively closed design wrt
>extension
This is exactly the reason (well one of the reasons ;-) why we don't
use it. Our code changes scheduling classes, thread priorities etc
and whilst this was a planned extension to Boost.Threads it still
isn't part of the released version - although this implies that
Boost.Threads would be a poor starting point, as Kevlin points out it
is the closed design that precludes wider adoption.

>Considering first the question of the threading part, Boost.Threads is
>currently based on the idea that a thread is identified with an object
>that launches it. This notion is somewhat confused by the idea that on
>destruction the thread object is destroyed but the thread is not -- in
>other words the thread is not identified the thread object... except
>when it is. There needs to be a separation of concepts here

Yes, that's something that's always bothered me too. Having read
through the history I can understand why Bill Kempf ended up with what
he did - and having delved into the thread_dev branch I can also
understand where he was heading and it's rather different to where it
is today! If my understanding is correct the main reason for the
default constrctor is to support operations on threads from other
libraries, which doesn't seem very compelling imv. There is some
value in the argument that it would also support operations on the
primary thread but in many cases it's hard/impossible to do much to
the primary thread anyway . . .

>Another appropriate separation is the distinction between initialisation
>and execution. These are significantly different concepts but they are
>conflated in the existing thread-launching interface: the constructor is
>responsible both for preparing the thread and launching it, which means
>that it is not possible for one piece of code to set up a thread and
>another to initiate it separately at its own discretion, eg thread
>pools.

When I first saw Boost.Threads use of functors it was clearly better
then anything I'd used before . . . Kevlins ideas take this one step
further though, which seems to make sense.

>The separation also offers a simple and non-intrusive avenue for
>platform-specific extension of how a thread is to execute: configuration
>details such as scheduling policy, stack size, security attributes, etc,
>can be added as constructors without intruding on the signatures of any
>other function in the threading interface:
>
> size_t stack_size = ...;
> security_attributes security(...);
> thread async_function(stack_size, security);

That's good.

<snip explanation of joiner>

>Via the joiner the initiator can poll or wait for the completion of the
>running thread, and control it in other ways -- eg request cancellation
>of the task (should we chose to attempt standardising in some form this
>often contentious and challenging capability).
>
>The joiner would be a copyable, assignable and default constructible
>handle, and as its principal action the act of joining can be expressed
>as a function-call:
>
> joiner wait = run(first_task);
> ...
> wait();
But if I wanted to store a joiner in an stl container then I might
need comparison operators too. The thread_dev branch shows that Bill
Kempf had worked really hard to get a 'thread' object (meaning an
object that can manipulate and join a running thread) that supported
this. I've never needed this but the archives suggest that some
people do ;-)

>If there are no joiners for a given thread, that thread is detached
<snip>
> It seems to have become habit to discard [the return] value in more
>OO interfaces, giving a result of void.

Conceptually a detached thread with no joiner sounds good, however
I've never actually investigated whether a detached thread concept is
portable - does an application with a detached posix thread behave the
same way as one with a detached WIN32 thread? This might be
important, since Kevlin suggests that many thread libraries ignore the
return value (I agree) and that may imply that it's accepted practice
to ignore the return value and therefore many developers porting code
to this model might by default create detached threads (whether
intentional or not, this would be the result of ignoring the return
value). Which if the behaviour is different might be bad :-(

>This leads to a threader interface like the following:
>
> class threader
> {
> public:
> threader();
> template<typename nullary_function>
> joiner<result_of<nullary_function>::type>
> operator()(nullary_function);
> ...
> };

It's arguable whether the threader object provides much additonal
value though, since it's sole purpose seems to be to provide a
placeholder for the 'launch' function. So every time I want to start
a new thread I have to do:

int task();
...
threader launcher;
joiner<int> result = launcher(task);

The obvious (and therefore no doubt flawed) alternative is:

joiner<int> result = launch(task);

Boost.Threads use of the constructor to launch the thread is no better
imv.

On second thoughts a threader does make sense some since it also
provides a placeholder for modifying the behaviour by providing an
alternative constructor.

> joiner<void> wait_for_completion = thread(void_task);
> joiner<int> wait_for_value = thread(int_task);

I still read this as a noun. Something like 'launch' might make the
action clearer and avoids possible confusion with the operating system
definition of 'thread' (which is a noun).

>Now, in terms of synchronisation the Boost.Threads library offers a
>number of primitives, such as mutexes, but unfortunately couples them to
>a relatively closed programming model. The synchronisation primitives
>provided do not lead to a general model for locking and force the user
>into using resource acquisition objects when this is not always the best
>solution. Given that C++ should aim to be the language and API of choice
>for systems-level work, the restriction of a mandatory scoped-locking
>interface does not seem appropriate.

Agreed again. It feels too protective and probably won't prevent
deadlock, whereas (for example) externally locking might.

The thread_dev branch of Boost.Threads contained a lot of work in
progress. Although Michael Glassford and others have done a great job
to implement some of this, some key parts (copyable 'thread handles',
cancellation etc) have not made it to release so we already know that
there is a better version of Boost.Thread than the current one -
changing to copyable thread handles is a big change which further
suggests that it's a good reason not to standardise the current
version of Boost.Thread. I've studied the thread_dev branch in some
detail and I can see why Bill Kempf chose to use thread local storage
to store thread state and I can see why he chose to use macros to
exclude/include thread priority functions. This does create a whole
lot of other problems though (e.g. TLS cleanup, solved for some
platforms but not all) which would create a rather 'heavyweight'
solution that doesn't always seem appropriate.

Conclusion: Boost.Threads is a big step forward conceptually but
isn't quite flexible enough and is also unfinished. It's not suitable
for the majority of our applications in it's current form although as
Kevlin points out it wouldn't take a huge shift to provide a more
useful starting point.

Malcolm Noyes


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk