Fully transitive const is not necessary

Sean Kelly sean at invisibleduck.org
Wed Apr 2 17:35:08 PDT 2008


== Quote from Walter Bright (newshound1 at digitalmars.com)'s article
> Sean Kelly wrote:
> > My traditional argument in support of logical const is this:
> >
> >     class C
> >     {
> >         mutable mutex monitor;
> >         std::string name;
> >     public:
> >         std::string name() const
> >         {
> >             scoped_lock sl( monitor );
> >             return name;
> >         }
> >
> >         void name( std::string const& n )
> >         {
> >             scoped_lock sl( monitor );
> >             name = n;
> >         }
> >     };
> >
> > Here, the mutex has nothing to do with the state of the object and
> > must be modified even during logically non-modifying operations.
> > Similar behavior might be necessary for a logging system in some
> > instances, etc.  However, I'm not certain whether this is sufficient
> > to justify logical const in general.  As you say--it has some serious
> > problems as well.
> If C.name were invariant, there would be no need for any locks. I don't
> think this is a good example, because with D's invariant strings there
> is no need for such locking.

I'm not sure that's true.  Mutexes do two things: first, they guarantee
atomicity for an arbitrary sequence of instructions and second, they
provide a memory barrier so the result of those instructions is made
visible to any other thread which later acquires the same mutex.  The
invariant label provides a slightly different guarantee: that the data
underlying an invariant reference will not ever change.  Here's an
off-the-cuff example:

    string getInput()
    {
        auto buf = new char[64]; // A
        readln( buf );
        return cast(string) buf;
    }

    class Person
    {
        string name() { return m_name; }
        void name( string n ) { name = n; }
        private string m_name;
    }

    auto p = new Person;
    // start Thread B

    // Thread A
    writef( "enter your name: " );
    p.name = getInput();

    // Thread B
    while( !p.name )
        Thread.yield();
    writefln( "Your name is ", p.name );

In the above code, the only memory barrier exists at point A, where the
GC acquires a mutex to handle the allocation (let's assume that no IO
synchronization occurs for the sake of this example).  After this memory
barrier, Thread A does this:

    * transfers input from the keyboard into buf
    * declares buf to be invariant because it will never change
    * assigns buf to the name member in p

Meanwhile, Thread B is waiting for name to be non-empty, but specifically,
what I believe it's doing is this:

    while p.name.ptr is null
        wait

Then it prints p.name to the screen using the length attribute to determine
how many bytes to print.  In this example, then, we are relying on the
following set of operations to occur sequentially, from a global perspective:

    1. the input to be written into buf
    2. the length of p.name to be set to the length of buf
    3. the ptr of p.name to be set to the ptr of buf

(note that operations 2 and 3 are actually expected to be a single atomic
operation, but I've ordered them this way based on how the code in Thread
B is written)

However, as you're no doubt aware, the underlying hardware and even
the generated code dictate the order in which things actually occur.  So
it's theoretically possible for the above operations to happen in any order.
What the mutex does in my original example is provide for two things:

    1. that the assignment of p.name will be logically atomic for users of p
    2. that any operation that happens before p.name is assigned in Thread
         A will be completed before the mutex is released

Thus, with the simple addition of a mutex within the p.name get/set
operations above, the code is actually safe and correct (though obviously
slower, since Thread B is spinning on a mutex lock).

I'll admit that in most real code, however, issue 1 will not exist because the
buffer returned fro getInput will be created via an idup operation, which
again involves a mutex-protected GC operation (with the current GC
anyway).  So the example was a bit contrived, but I feel it does illustrate
the point that invariance as an optional attribute isn't a panacea for
multiprogramming.

At the very least with an imperative language, there must be some means
of guaranteeing expected behavior for shared data.  One route would be to
have a fairly strict memory model (like Java) and another would be to provide
constructs whereby the programmer can indicate the order of operations
('volatile' in D).  C++ 0x will be somewhere in the middle by providing specific
atomic types and operations without dictating too much about the relative order
of other operations (last time I checked anyway--I haven't read the final proposal).

And please forgive me if I'm belaboring the obvious.  It was easier to just cover
the topic from end to end than to make assumptions about what was mutually
understood.


Sean



More information about the Digitalmars-d mailing list