Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: downloads/boost_1_34_1/libs/thread/doc/rationale.xml @ 33

Last change on this file since 33 was 29, checked in by landauf, 16 years ago

updated boost from 1_33_1 to 1_34_1

File size: 22.3 KB
Line 
1<?xml version="1.0" encoding="utf-8"?>
2<!DOCTYPE library PUBLIC "-//Boost//DTD BoostBook XML V1.0//EN"
3  "http://www.boost.org/tools/boostbook/dtd/boostbook.dtd" [
4  <!ENTITY % thread.entities SYSTEM "entities.xml">
5  %thread.entities;
6]>
7<!-- Copyright (c) 2002-2003 William E. Kempf, Michael Glassford
8     Subject to the Boost Software License, Version 1.0.
9     (See accompanying file LICENSE-1.0 or  http://www.boost.org/LICENSE-1.0)
10-->
11<section id="thread.rationale" last-revision="$Date: 2006/10/15 14:52:53 $">
12  <title>Rationale</title>
13  <para>This page explains the rationale behind various design decisions in the
14  &Boost.Thread; library. Having the rationale documented here should explain
15  how we arrived at the current design as well as prevent future rehashing of
16  discussions and thought processes that have already occurred. It can also give
17  users a lot of insight into the design process required for this
18  library.</para>
19  <section id="thread.rationale.Boost.Thread">
20    <title>Rationale for the Creation of &Boost.Thread;</title>
21    <para>Processes often have a degree of "potential parallelism" and it can
22        often be more intuitive to design systems with this in mind. Further, these
23        parallel processes can result in more responsive programs. The benefits for
24        multithreaded programming are quite well known to most modern programmers,
25        yet the C++ language doesn't directly support this concept.</para>
26    <para>Many platforms support multithreaded programming despite the fact that
27        the language doesn't support it. They do this through external libraries,
28        which are, unfortunately, platform specific. POSIX has tried to address this
29        problem through the standardization of a "pthread" library. However, this is
30        a standard only on POSIX platforms, so its portability is limited.</para>
31    <para>Another problem with POSIX and other platform specific thread
32        libraries is that they are almost universally C based libraries. This leaves
33        several C++ specific issues unresolved, such as what happens when an
34        exception is thrown in a thread. Further, there are some C++ concepts, such
35        as destructors, that can make usage much easier than what's available in a C
36        library.</para>
37    <para>What's truly needed is C++ language support for threads. However, the
38        C++ standards committee needs existing practice or a good proposal as a
39        starting point for adding this to the standard.</para>
40    <para>The &Boost.Thread; library was developed to provide a C++ developer
41        with a portable interface for writing multithreaded programs on numerous
42        platforms. There's a hope that the library can be the basis for a more
43        detailed proposal for the C++ standards committee to consider for inclusion
44        in the next C++ standard.</para>
45  </section>
46  <section id="thread.rationale.primitives">
47    <title>Rationale for the Low Level Primitives Supported in &Boost.Thread;</title>
48    <para>The &Boost.Thread; library supplies a set of low level primitives for
49        writing multithreaded programs, such as mutexes and condition variables. In
50        fact, the first release of &Boost.Thread; supports only these low level
51        primitives. However, computer science research has shown that use of these
52        primitives is difficult since it's difficult to mathematically prove that a
53        usage pattern is correct, meaning it doesn't result in race conditions or
54        deadlocks. There are several algebras (such as CSP, CCS and Join calculus)
55        that have been developed to help write provably correct parallel
56        processes. In order to prove the correctness these processes must be coded
57        using higher level abstractions. So why does &Boost.Thread; support the
58        lower level concepts?</para>
59    <para>The reason is simple: the higher level concepts need to be implemented
60        using at least some of the lower level concepts. So having portable lower
61        level concepts makes it easier to develop the higher level concepts and will
62        allow researchers to experiment with various techniques.</para>
63    <para>Beyond this theoretical application of higher level concepts, however,
64        the fact remains that many multithreaded programs are written using only the
65        lower level concepts, so they are useful in and of themselves, even if it's
66        hard to prove that their usage is correct. Since many users will be familiar
67        with these lower level concepts but unfamiliar with any of the higher
68        level concepts, supporting the lower level concepts provides
69        greater accessibility.</para>
70  </section>
71  <section id="thread.rationale.locks">
72    <title>Rationale for the Lock Design</title>
73    <para>Programmers who are used to multithreaded programming issues will
74        quickly note that the &Boost.Thread; design for mutex lock concepts is not
75        <link linkend="thread.glossary.thread-safe">thread-safe</link> (this is
76        clearly documented as well). At first this may seem like a serious design
77        flaw. Why have a multithreading primitive that's not thread-safe
78        itself?</para>
79    <para>A lock object is not a synchronization primitive. A lock object's sole
80        responsibility is to ensure that a mutex is both locked and unlocked in a
81        manner that won't result in the common error of locking a mutex and then
82        forgetting to unlock it. This means that instances of a lock object are only
83        going to be created, at least in theory, within block scope and won't be
84        shared between threads. Only the mutex objects will be created outside of
85        block scope and/or shared between threads. Though it's possible to create a
86        lock object outside of block scope and to share it between threads, to do so
87        would not be a typical usage (in fact, to do so would likely be an
88        error). Nor are there any cases when such usage would be required.</para>
89    <para>Lock objects must maintain some state information. In order to allow a
90        program to determine if a try_lock or timed_lock was successful the lock
91        object must retain state indicating the success or failure of the call made
92        in its constructor. If a lock object were to have such state and remain
93        thread-safe it would need to synchronize access to the state information
94        which would result in roughly doubling the time of most operations. Worse,
95        since checking the state can occur only by a call after construction, we'd
96        have a race condition if the lock object were shared between threads.</para>
97    <para>So, to avoid the overhead of synchronizing access to the state
98        information and to avoid the race condition, the &Boost.Thread; library
99        simply does nothing to make lock objects thread-safe. Instead, sharing a
100        lock object between threads results in undefined behavior. Since the only
101        proper usage of lock objects is within block scope this isn't a problem, and
102        so long as the lock object is properly used there's no danger of any
103        multithreading issues.</para>
104  </section>
105  <section id="thread.rationale.non-copyable">
106    <title>Rationale for NonCopyable Thread Type</title>
107    <para>Programmers who are used to C libraries for multithreaded programming
108        are likely to wonder why &Boost.Thread; uses a noncopyable design for
109        <classname>boost::thread</classname>. After all, the C thread types are
110        copyable, and you often have a need for copying them within user
111        code. However, careful comparison of C designs to C++ designs shows a flaw
112        in this logic.</para>
113    <para>All C types are copyable. It is, in fact, not possible to make a
114        noncopyable type in C. For this reason types that represent system resources
115        in C are often designed to behave very similarly to a pointer to dynamic
116        memory. There's an API for acquiring the resource and an API for releasing
117        the resource. For memory we have pointers as the type and alloc/free for
118        the acquisition and release APIs. For files we have FILE* as the type and
119        fopen/fclose for the acquisition and release APIs. You can freely copy
120        instances of the types but must manually manage the lifetime of the actual
121        resource through the acquisition and release APIs.</para>
122    <para>C++ designs recognize that the acquisition and release APIs are error
123        prone and try to eliminate possible errors by acquiring the resource in the
124        constructor and releasing it in the destructor. The best example of such a
125        design is the std::iostream set of classes which can represent the same
126        resource as the FILE* type in C. A file is opened in the std::fstream's
127        constructor and closed in its destructor. However, if an iostream were
128        copyable it could lead to a file being closed twice, an obvious error, so
129        the std::iostream types are noncopyable by design. This is the same design
130        used by boost::thread, which is a simple and easy to understand design
131        that's consistent with other C++ standard types.</para>
132    <para>During the design of boost::thread it was pointed out that it would be
133        possible to allow it to be a copyable type if some form of "reference
134        management" were used, such as ref-counting or ref-lists, and many argued
135        for a boost::thread_ref design instead. The reasoning was that copying
136        "thread" objects was a typical need in the C libraries, and so presumably
137        would be in the C++ libraries as well. It was also thought that
138        implementations could provide more efficient reference management than
139        wrappers (such as boost::shared_ptr) around a noncopyable thread
140        concept. Analysis of whether or not these arguments would hold true doesn't
141        appear to bear them out. To illustrate the analysis we'll first provide
142        pseudo-code illustrating the six typical usage patterns of a thread
143        object.</para>
144        <section id="thread.rationale.non-copyable.simple">
145          <title>1. Use case: Simple creation of a thread.</title>
146      <programlisting>
147      void foo()
148      {
149         create_thread(&amp;bar);
150      }
151      </programlisting>
152        </section>
153        <section id="thread.rationale.non-copyable.joined">
154          <title>2. Use case: Creation of a thread that's later joined.</title>
155      <programlisting>
156      void foo()
157      {
158         thread = create_thread(&amp;bar);
159         join(thread);
160      }
161      </programlisting>
162        </section>
163        <section id="thread.rationale.non-copyable.loop">
164          <title>3. Use case: Simple creation of several threads in a loop.</title>
165      <programlisting>
166      void foo()
167      {
168         for (int i=0; i&lt;NUM_THREADS; ++i)
169            create_thread(&amp;bar);
170      }
171      </programlisting>
172        </section>
173        <section id="thread.rationale.non-copyable.loop-join">
174          <title>4. Use case: Creation of several threads in a loop which are later joined.</title>
175      <programlisting>
176      void foo()
177      {
178         for (int i=0; i&lt;NUM_THREADS; ++i)
179            threads[i] = create_thread(&amp;bar);
180         for (int i=0; i&lt;NUM_THREADS; ++i)
181            threads[i].join();
182      }
183      </programlisting>
184        </section>
185        <section id="thread.rationale.non-copyable.pass">
186          <title>5. Use case: Creation of a thread whose ownership is passed to another object/method.</title>
187      <programlisting>
188      void foo()
189      {
190         thread = create_thread(&amp;bar);
191         manager.owns(thread);
192      }
193      </programlisting>
194        </section>
195        <section id="thread.rationale.non-copyable.shared">
196          <title>6. Use case: Creation of a thread whose ownership is shared between multiple
197          objects.</title>
198          <programlisting>
199      void foo()
200      {
201         thread = create_thread(&amp;bar);
202         manager1.add(thread);
203         manager2.add(thread);
204      }
205      </programlisting>
206        </section>
207    <para>Of these usage patterns there's only one that requires reference
208        management (number 6). Hopefully it's fairly obvious that this usage pattern
209        simply won't occur as often as the other usage patterns. So there really
210        isn't a "typical need" for a thread concept, though there is some
211        need.</para>
212    <para>Since the need isn't typical we must use different criteria for
213        deciding on either a thread_ref or thread design. Possible criteria include
214        ease of use and performance. So let's analyze both of these
215        carefully.</para>
216    <para>With ease of use we can look at existing experience. The standard C++
217        objects that represent a system resource, such as std::iostream, are
218        noncopyable, so we know that C++ programmers must at least be experienced
219        with this design. Most C++ developers are also used to smart pointers such
220        as boost::shared_ptr, so we know they can at least adapt to a thread_ref
221        concept with little effort. So existing experience isn't going to lead us to
222        a choice.</para>
223    <para>The other thing we can look at is how difficult it is to use both
224        types for the six usage patterns above. If we find it overly difficult to
225        use a concept for any of the usage patterns there would be a good argument
226        for choosing the other design. So we'll code all six usage patterns using
227        both designs.</para>
228        <section id="thread.rationale_comparison.non-copyable.simple">
229          <title>1. Comparison: simple creation of a thread.</title>
230          <programlisting>
231      void foo()
232      {
233         thread thrd(&amp;bar);
234      }
235      void foo()
236      {
237         thread_ref thrd = create_thread(&amp;bar);
238      }
239      </programlisting>
240        </section>
241        <section id="thread.rationale_comparison.non-copyable.joined">
242          <title>2. Comparison: creation of a thread that's later joined.</title>
243          <programlisting>
244      void foo()
245      {
246         thread thrd(&amp;bar);
247         thrd.join();
248      }
249      void foo()
250      {
251         thread_ref thrd =
252         create_thread(&amp;bar);thrd-&gt;join();
253      }
254      </programlisting>
255        </section>
256        <section id="thread.rationale_comparison.non-copyable.loop">
257          <title>3. Comparison: simple creation of several threads in a loop.</title>
258      <programlisting>
259      void foo()
260      {
261         for (int i=0; i&lt;NUM_THREADS; ++i)
262            thread thrd(&amp;bar);
263      }
264      void foo()
265      {
266         for (int i=0; i&lt;NUM_THREADS; ++i)
267            thread_ref thrd = create_thread(&amp;bar);
268      }
269      </programlisting>
270        </section>
271        <section id="thread.rationale_comparison.non-copyable.loop-join">
272          <title>4. Comparison: creation of several threads in a loop which are later joined.</title>
273      <programlisting>
274      void foo()
275      {
276         std::auto_ptr&lt;thread&gt; threads[NUM_THREADS];
277         for (int i=0; i&lt;NUM_THREADS; ++i)
278            threads[i] = std::auto_ptr&lt;thread&gt;(new thread(&amp;bar));
279         for (int i= 0; i&lt;NUM_THREADS;
280             ++i)threads[i]-&gt;join();
281      }
282      void foo()
283      {
284         thread_ref threads[NUM_THREADS];
285         for (int i=0; i&lt;NUM_THREADS; ++i)
286            threads[i] = create_thread(&amp;bar);
287         for (int i= 0; i&lt;NUM_THREADS;
288            ++i)threads[i]-&gt;join();
289      }
290      </programlisting>
291        </section>
292        <section id="thread.rationale_comparison.non-copyable.pass">
293          <title>5. Comparison: creation of a thread whose ownership is passed to another object/method.</title>
294      <programlisting>
295      void foo()
296      {
297         thread thrd* = new thread(&amp;bar);
298         manager.owns(thread);
299      }
300      void foo()
301      {
302         thread_ref thrd = create_thread(&amp;bar);
303         manager.owns(thrd);
304      }
305      </programlisting>
306        </section>
307        <section id="thread.rationale_comparison.non-copyable.shared">
308          <title>6. Comparison: creation of a thread whose ownership is shared
309          between multiple objects.</title>
310      <programlisting>
311      void foo()
312      {
313         boost::shared_ptr&lt;thread&gt; thrd(new thread(&amp;bar));
314         manager1.add(thrd);
315         manager2.add(thrd);
316      }
317      void foo()
318      {
319         thread_ref thrd = create_thread(&amp;bar);
320         manager1.add(thrd);
321         manager2.add(thrd);
322      }
323      </programlisting>
324        </section>
325    <para>This shows the usage patterns being nearly identical in complexity for
326        both designs. The only actual added complexity occurs because of the use of
327        operator new in
328        <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link>,
329        <link linkend="thread.rationale_comparison.non-copyable.pass">(5)</link>, and
330        <link linkend="thread.rationale_comparison.non-copyable.shared">(6)</link>;
331        and the use of std::auto_ptr and boost::shared_ptr in
332        <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link> and
333        <link linkend="thread.rationale_comparison.non-copyable.shared">(6)</link>
334        respectively. However, that's not really
335        much added complexity, and C++ programmers are used to using these idioms
336        anyway. Some may dislike the presence of operator new in user code, but
337        this can be eliminated by proper design of higher level concepts, such as
338        the boost::thread_group class that simplifies example
339        <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link>
340        down to:</para>
341    <programlisting>
342    void foo()
343    {
344       thread_group threads;
345       for (int i=0; i&lt;NUM_THREADS; ++i)
346          threads.create_thread(&amp;bar);
347       threads.join_all();
348    }
349    </programlisting>
350    <para>So ease of use is really a wash and not much help in picking a
351        design.</para>
352    <para>So what about performance? Looking at the above code examples,
353    we can analyze the theoretical impact to performance that both designs
354        have. For <link linkend="thread.rationale_comparison.non-copyable.simple">(1)</link>
355        we can see that platforms that don't have a ref-counted native
356        thread type (POSIX, for instance) will be impacted by a thread_ref
357        design. Even if the native thread type is ref-counted there may be an impact
358        if more state information has to be maintained for concepts foreign to the
359        native API, such as clean up stacks for Win32 implementations.
360        For <link linkend="thread.rationale_comparison.non-copyable.joined">(2)</link>
361        and <link linkend="thread.rationale_comparison.non-copyable.loop">(3)</link>
362        the performance impact will be identical to
363        <link linkend="thread.rationale_comparison.non-copyable.simple">(1)</link>.
364        For <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link>
365        things get a little more interesting and we find that theoretically at least
366        the thread_ref may perform faster since the thread design requires dynamic
367        memory allocation/deallocation. However, in practice there may be dynamic
368        allocation for the thread_ref design as well, it will just be hidden from
369        the user. As long as the implementation has to do dynamic allocations the
370        thread_ref loses again because of the reference management. For
371        <link linkend="thread.rationale_comparison.non-copyable.pass">(5)</link> we see
372        the same impact as we do for
373        <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link>.
374        For <link linkend="thread.rationale_comparison.non-copyable.shared">(6)</link>
375        we still have a possible impact to
376        the thread design because of dynamic allocation but thread_ref no longer
377        suffers because of its reference management, and in fact, theoretically at
378        least, the thread_ref may do a better job of managing the references. All of
379        this indicates that thread wins for
380        <link linkend="thread.rationale_comparison.non-copyable.simple">(1)</link>,
381        <link linkend="thread.rationale_comparison.non-copyable.joined">(2)</link> and
382        <link linkend="thread.rationale_comparison.non-copyable.loop">(3)</link>; with
383        <link linkend="thread.rationale_comparison.non-copyable.loop-join">(4)</link>
384        and <link linkend="thread.rationale_comparison.non-copyable.pass">(5)</link> the
385        winner depending on the implementation and the platform but with the thread design
386        probably having a better chance; and with
387        <link linkend="thread.rationale_comparison.non-copyable.shared">(6)</link> 
388        it will again depend on the
389        implementation and platform but this time we favor thread_ref
390        slightly. Given all of this it's a narrow margin, but the thread design
391        prevails.</para>
392        <para>Given this analysis, and the fact that noncopyable objects for system
393        resources are the normal designs that C++ programmers are used to dealing
394        with, the &Boost.Thread; library has gone with a noncopyable design.</para>
395  </section>
396  <section id="thread.rationale.events">
397    <title>Rationale for not providing <emphasis>Event Variables</emphasis></title>
398    <para><emphasis>Event variables</emphasis> are simply far too
399        error-prone. <classname>boost::condition</classname> variables are a much safer
400        alternative. [Note that Graphical User Interface <emphasis>events</emphasis> are
401        a different concept, and are not what is being discussed here.]</para>
402    <para>Event variables were one of the first synchronization primitives. They
403        are still used today, for example, in the native Windows multithreading
404        API. Yet both respected computer science researchers and experienced
405        multithreading practitioners believe event variables are so inherently
406        error-prone that they should never be used, and thus should not be part of a
407        multithreading library.</para>
408    <para>Per Brinch Hansen &cite.Hansen73; analyzed event variables in some
409        detail, pointing out [emphasis his] that "<emphasis>event operations force
410        the programmer to be aware of the relative speeds of the sending and
411        receiving processes</emphasis>". His summary:</para>
412    <blockquote>
413      <para>We must therefore conclude that event variables of the previous type
414          are impractical for system design. <emphasis>The effect of an interaction
415          between two processes must be independent of the speed at which it is
416          carried out.</emphasis></para>
417    </blockquote>
418    <para>Experienced programmers using the Windows platform today report that
419        event variables are a continuing source of errors, even after previous bad
420        experiences caused them to be very careful in their use of event
421        variables. Overt problems can be avoided, for example, by teaming the event
422        variable with a mutex, but that may just convert a <link
423        linkend="thread.glossary.race-condition">race condition</link> into another
424        problem, such as excessive resource use. One of the most distressing aspects
425        of the experience reports is the claim that many defects are latent. That
426        is, the programs appear to work correctly, but contain hidden timing
427        dependencies which will cause them to fail when environmental factors or
428        usage patterns change, altering relative thread timings.</para>
429    <para>The decision to exclude event variables from &Boost.Thread; has been
430        surprising to some Windows programmers. They have written programs which
431        work using event variables, and wonder what the problem is. It seems similar
432        to the "goto considered harmful" controversy of 30 years ago. It isn't that
433        events, like gotos, can't be made to work, but rather that virtually all
434        programs using alternatives will be easier to write, debug, read, maintain,
435        and will be less likely to contain latent defects.</para>
436    <para>[Rationale provided by Beman Dawes]</para>
437  </section>
438</section>
Note: See TracBrowser for help on using the repository browser.