
With Kevlin Henney's permission, the following is a repost of a message he recently posted on the C++ committee's library reflector. It gives his views on Boost.Threads in the context of possible standardization. --Beman ------- In considering a threading library for C++ based on a fairly standard set of primitives, there are essentially two parts to such a library and it is worth considering these separately: the actual threading part, ie the part of the library concerned with the creation, execution and management of separate threads of execution; the synchronisation part, ie the part of the library concerned with locking. The two parts are obviously related in terms of execution, but there is no necessary relationship between them in terms of interface dependencies. These two parts are also in addition to the necessary guarantees of a reasonable memory model and other primitives appropriate for lock-free programming, which are also under related but separate discussion. For the C++ standard library a reasonable goal for both the threading and the synchronisation parts would be to adopt a generic-programming style of design. Although Boost.Threads uses templates in expressing some of its capabilities, it is a relatively closed design wrt extension, and hence not what would normally be termed generic. However, Boost.Threads does offer a starting point, and with only a few considerations and changes it is relatively easy to evolve a more open and appropriate design from it. 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, which I will come back to in a moment. 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. Separating the two roles into constructor and executor function clears up both the technical and the conceptual issue. The executor function can be reasonably expressed as an overloaded function-call operator: void task(); ... thread async_function; ... asynch_function(task); 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); The default constructor would be the feature standardised, and an implementation would be free to add additional constructors as appropriate. So far, this leads to an interface that looks something like the following: class thread { public: thread(); template<typename nullary_function> void operator()(nullary_function); void join(); ... }; Given that the same configuration might be used to launch other threads, and given the identity confusion of a thread being an object except when it's not, we can consider the interface not to be the interface of a thread but to be the interface of a thread launcher, also generally known as an executor: http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/Executor.htm l http://www.cse.buffalo.edu/~crahen/papers/Executor.Pattern.pdf A thread initiator can submit zero-argument functions and function objects to an executor for execution: threader run; ... run(first_task); run(second_task); A standard library would offer a standardised threader type, but as a concept an executor could be implemented in a variety of ways that still conform to the same basic launching interface, ie the function-call operator. As a pleasing side effect, the potential confusion over the name "thread" is also removed: entities are named after their roles. Given that a threader can be used to launch multiple threads, there is the obvious question of how to join with each separately run thread. Instead of returning void, the threader returns an object whose primary purpose is to represent the ability to join with the completion of a separately executing thread of control: class threader { public: threader(); template<typename nullary_function> joiner operator()(nullary_function); ... }; The role played by the joiner in this fragment is that of an asynchronous completion token, a common pattern for synchronising with and controlling asynchronous tasks: http://www.cs.wustl.edu/~schmidt/PDF/ACT.pdf 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(); If there are no joiners for a given thread, that thread is detached, a role currently played in Boost.Threads by the thread destructor: run(second_task); // runs detached because return value ignored The final piece in this sketched refinement is the recognition that common across many C threading APIs is the ability to return a result from the function executed asynchronously (void * in Pthreads and DWORD in Win32). It seems to have become habit to discard this value in more OO interfaces, giving a result of void. For many threaded tasks this makes sense, but where a thread is working towards a result then the idea that an asynchronously executed function can return a value for later collection should not be discarded. With a void-returning interface the programmer is forced to set up an arrangement for the threaded task to communicate a value to the party that wants the one-time result. This is tedious for such a simple case, and can be readily catered for by making the joiner a proper future variable that proxies for the result: http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/Future.html http://www.laputan.org/pub/sag/act-obj.pdf http://kasparov.skife.org/blog/src/futures.html 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); ... }; For the common default configured threader, a wrapper function, thread (as verb rather than noun), can be provided: template<typename nullary_function> joiner<result_of<nullary_function>::type> thread(nullary_function); And usage as follows: void void_task(); int int_task(); ... joiner<void> wait_for_completion = thread(void_task); joiner<int> wait_for_value = thread(int_task); ... int result = wait_for_value(); wait_for_completion(); The benefit of programming with futures is that for a certain class of code that would use end-of-thread synchronisation to pick up results, programmers are not presented with unnecessarily low-level synchronisation APIs. The function-based model is applied consistently. The sequence of steps, and the design considerations involved, to get from Boost.Threads to this design is hopefully clear and can be seen to be grounded in a great deal of existing practice, plus the benefit of a more native C++ appearance. 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. A more orthogonal approach based on the capabilities found in common across C++ synchronisation libraries, Boost.Threads included, would be a good starting point: lockability and locking strategy are kept separate. Taking a more generic approach, this means considering a related set of lock categories, eg Lockable and TryLockable. Something like Lockable would (syntactically) be little more than a.lock() and a.unlock(), and something like TryLockable would extend with a.try_lock(). Boost.Threads has some of this but restricts the interface of the lockable types themselves. With respect to these categories both primitives (eg mutex) and higher-level types (other externally locked objects) can be implemented. There is no dependency on the locking strategies that can be used against lockable objects -- scope bound, transaction linked, smart-pointer based, etc. The library would be expected to provide at least scope lockers to simplify the tedious and error-prone business of ensuring that scope-related critical regions are exception safe, but programmers would be free to define additional ones based on specific needs. There is also no requirement to pollute the nested scope of a lockable class with special typedefs. The Boost.Threads synchronisation library does not appear to satisfy the goal of openness and genericity. This is in part based on its well intended but mistaken belief that preventing programmers from using locks manually is the key to safe code: preventing users from using multiple threads and locks is the only way to do this. It is easy but annoying to work around the restriction (eg dynamically allocate the locker objects and control the lifetime on the heap instead of automatically wrt scope), but it should not be something that a programmer should have to work around. The Boost.Threads documentation itself admits that its restricted approach is extreme, but a more open and proven -- and less extreme -- approach is probably a better fit for standardisation. Therefore, the design we need to aim for is one that recognises that any use of explicit threading or any form of synchronisation is the programmer's responsibility. It should make common tasks easier and less error prone than they might otherwise be -- eg the provision of scope lockers -- but it should also allow the programmer the freedom to implement locking strategies as they see fit rather than force them to work around the API or seek another. I've only sketched it out, but hopefully there is enough rationale there to see why, without some refinement, Boost.Threads is not quite the right interface and execution model to standardise, but also how few hops it takes to build from Boost.Threads to an alternative API model that meets a reasonable and reasoned set of design objectives. Kevlin --

Kevlin Henney wrote:
Although Boost.Threads uses templates in expressing some of its capabilities, it is a relatively closed design wrt extension, and hence not what would normally be termed generic.
A partial list of features I expect in the documented interface of an open thread library: 1) Ability to define my own thread implementation that is compatible and cohesive with the included implementation 2) Ability to use more than one implementation at once in different parts of the code. (For example, on Cygwin, you could conceivably use POSIX threads and Win32 threads in the same application.) 3) Ability to extend the existing implementations to take advantage of underlying features of the threading implementation that may require private thread handles 4) Cohesiveness with arbitrary demultiplexors. (For example, there should be some adaptor that lets me wait for threads in an application based on the popular libevent library.) 5) Ability to dynamically load any of the above parts after execution has started, so arbitrary implementations might be provided (possibly for some additional penalty). Related to #3 above, I am wondering what the standard committee's current feeling is on providing some way for user code to access inner handles of standard library abstractions. Of filebuf, this is a fairly common request. I've been thinking about Dave Abraham's suggestion <http://lists.boost.org/boost/2005/03/21839.php> that theres no reason to provide a special protection mechanism for these inner handles. The feeling I get from many here is that you should be able to hit a red button, such as including a special implementation header, and you're back in shoot-yourself-in-the-foot land, with the confidence that "the programmer knows what he's doing." I've decided that this is not compelling for me. We always know what we're doing--we're superheroes, we're C++ programmers. Yet C++ is partly about letting us define a set of rules for ourselves, to catch our mistakes, and keep us honest. I think the ability to hit this red button and suddenly have full license to blow away a class's invariants without any of the traditional protection mechanisms is downright barbaric. I think there *is* something special about inheritance. The "protected" keyword gets wonderful milage in other application domains, and I have no idea why system abstractions need to be any different, such that for system dependence, its perfectly acceptable to revert to pre-Dijkstraesque programming practices. Certainly noone would tolerate this additude with other well-defined standard libraries: no matter how much you beg, you just aren't going to get anyone to give you a pointer to std::list's linked list head. People point out that shared_ptr can be persuaded to give up it's pointer, to let the programmer do what he needs if required. However, this isn't the same; the pointer isn't actually part of the implementation. For example, you'll never be able to directly manipulate its reference count, even if you pull rank and flash your system-specific badge. Maybe inheritance isn't the right mechanism. But I think there should be something that isn't as heavy-handed as just giving up and stripping down. I think that once an acceptable mechanism is invented, it could be applied across the board to not just thread, but std::filebuf, boost::filesystem, sockets, and other similar components. Sorry for the length, Aaron W. LaFramboise

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);
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
That's good. <snip explanation of joiner> 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
participants (3)
-
Aaron W. LaFramboise
-
Beman Dawes
-
Malcolm Noyes