Boost logo

Boost :

From: Kevlin Henney (kevlin_at_[hidden])
Date: 2000-12-04 14:58:05


In message <90gbnn+104i5_at_[hidden]>, William Kempf <sirwillard_at_my-
deja.com> writes
>Because we're talking about thread safety here, giving code to
>illustrate the point would be much more complex than the description
>of the problem given.

I think seeing the code would be of more use, to be honest, as your
misunderstanding seems to be based on code I cannot see.

>Think about a string that's shared
>across threads.

No, think of a string that's not shared across threads. As I said, there
are many subtleties to programming with threads and objects, and if you
are going to share mutable, sequential value types between threads you
have just fallen into one of them. Sorry, but that's just the way it is
:-(

This discussion does not simply apply to the function wrapper, it is a
multi-threading design consideration in general. Consider it for any
type T that has non-atomic copying and assignment semantics. Using a
common classification, we can treat objects as being passive (only
execute when called, have no thread of their own) or active (having
their own thread). There are different ways of classifying passive
objects, but for the sake of argument here is the way that UML
classifies operation synchronisation:

sequential: Safe in a single thread only. Needs external locking to be
used across threads. This includes value objects, such as strings.

guarded: Monitor-like and can be accessed across multiple threads.
Synchronisation is self managed and access is serialised.

concurrent: Multiple thread accesses without explicit locking are
possible. This is the case with access to immutable data and for lock-
free change operations, eg InterlockedIncrement.

The first category need either manual effort to ensure that everyone has
agreed on when and how they are going to lock it -- this is not
considered a safe practice -- or it needs to be encapsulated within an
object with guarded functions. The second and third category can be
shared freely.

The following class is monitor-like:

        class M
        {
        public:
                void set_t(const T & new_t)
                {
                        lock guard(*this); // exception-safe locker
                        t = new_t; // assignment in critical section
                }
                ...
        private:
                T t;
        };

And here is a function executing in any thread, passed a pointer to an
instance of M that may be shared:

        void f(M * m)
        {
                T t;
                m->set_t(t);
                ... // do things with t
        }

You will see that the copying is perfectly safe. Another scenario is
when kicking off an active object. Let's assume that we can take an
argless function object and launch a thread around it, and that thread
objects are themselves function objects. The object to be activated will
have completed its initialisation before the thread is running, and
copying will in no way be a problem:

        class F
        {
        public:
                F(const T & initial_t) : t(initial_t) {}
                void operator()()
                {
                        while(running())
                        {
                                ... // do things with t
                        }
                }
                ...
        private:
                T t;
        };

        int main()
        {
                T t;
                F f(t);
                thread run;
                run(f);
                ... // loop and do things with t
                return 0;
        }

So communicating with a monitor-like passive object is safe, as is
kicking off an active. The only other scenario that is reasonable to
consider is communicating with an active object (see Active Object
pattern in POSA 2, older versions also available on the web). To make
function calls asynch, a function object is composed in the called
function and enqueued to be picked up when the operator() of the active
object is good and ready. Assuming a blocking dequeue, guarded enqueue
and, for brevity, the active object having its own threading features:

        class A
        {
        public:
                void set_t(const T & new_t)
                {
                        enqueue(&A::private_set_t, new_t);
                                // create and enqueue function object
                                // which holds pointer to member and
                                // copy of new_t
                }
                void operator()(); // launch thread
                ...
        private:
                void private_set_t(const T & t)
                {
                        t = new_t;
                }
                void main() // thread main
                {
                        while(running())
                        {
                                dequeue()(this);
                                ... // do things with t
                        }
                }
                ...
                T t;
        };

Once again, this is thread safe.

If T does not deep copy then it must refer to either immutable objects
or to objects with self-managed safety for the above examples to be
safe. If T refers indirectly to something that does not satisfy these,
then the user of T must be responsible for its synchronisation. Deep
copying objects that can be deep copied unasks the questions raised by
such issues.

Thus -- back to the exciting topic of this thread -- I don't believe we
should break good designs to support broken designs: copying is the
solution, sharing is the problem.

As an aside, you will see above two opportunities to use the kind of
wrapped function objects we are talking about: in the implementation of
thread and in the active object's queue.

>So, discussing thread safety as a reason to choose one over the other
>is a red herring, but if you insist going down that path it leads to
>ref-counting not cloning.

Threading came up as a topic for discussion. I merely demonstrated that
both threading and the sequential value-based approach lead to the same
conclusion: cloning. When a design decision can be made for two
different reasons this makes a very strong case for it and its
stability. If you are still unhappy with the threading case, I suggest
we drop it and just focus on the one we can agree on.

Kevlin
____________________________________________________________

  Kevlin Henney phone: +44 117 942 2990
  Curbralan Limited mobile: +44 7801 073 508
  mailto:kevlin_at_[hidden] fax: +44 870 052 2289
  http://www.curbralan.com
____________________________________________________________


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