Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: downloads/boost_1_33_1/libs/thread/doc/rationale.xml @ 12

Last change on this file since 12 was 12, checked in by landauf, 17 years ago

added boost

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