[cpp-threads] Prism 0.9.1, and further on the effects of races

Peter Dimov pdimov at mmltd.net
Fri Sep 22 02:49:19 BST 2006


Herb Sutter wrote:
> (Sorry for the lag, I'm on the road and email is intermittent.)
>
> Peter wrote:
>> Herb Sutter wrote:
>>>
>>>   thread 1: global_ptr = new Derived();
>>>
>>> With no constraints, this could execute as:
>>>
>>>             global_ptr = (Derived*)operator new( sizeof(Derived) );
>>>             global_ptr->vptr = &Base::vtable;     // a
>>>             global_ptr->Base ctor body;
>>>             global_ptr->vptr = &Derived::vtable;  // b
>>>             global_ptr->Derived ctor body;
>>>
>>> And in a race we perform:
>>>
>>>   thread 2: global_ptr->DerivedFunc();            // c
>>
>> This isn't a problem in either model A (undefined behavior) and
>> model B (global_ptr has an undefined value in the event of a race).
>> In both cases, the (c) statement is allowed to trap.
>>
>> It is only a problem if a read that participates in a race is
>> constrained to
>> only return one of the possible values that the variable could hold
>> if the race was resolved in a sequentially consistent manner, but
>> this is generally
>> not true for non-atomic variables (if it were, we wouldn't need raw
>> atomics.)
>
> Good points. For a moment, let's restrict to the case where
> global_ptr is a type that is known to be atomically read/written --
> either a proposed std::atomic<Derived*> or a plain Derived* on a
> platform that happens to document a guarantee that reads and writes
> from a plain default-aligned T* are atomic.
>
> In line c, the behaviors that I think will be unsurprising to users
> are: (1) a correct function call on a completely constructed object,
> and (2) a null pointer trap.
>
> The question is, should we also permit general undefined behavior,
> such as a wild branch?

If the store to global_ptr does not have release semantics, the behavior is 
undefined since thread 2 is not required to observe the updates to 
*global_ptr because they can become visible after the update to global_ptr.

If the store to global_ptr does have release semantics, on all hardware 
except Alpha thread 2 will observe either NULL or a fully constructed object 
because of the data dependency between global_ptr and *global_ptr. However, 
if Derived::Derived updates a global variable as a side effect, this update 
might not become visible to thread 2, unless the read from global_ptr has 
acquire semantics.

I think.

> Here's a different and maybe more useful way to put this question:
> Should line c be allowed any additional behaviors than if global_ptr
> pointed to a type having no virtual functions?

I don't see why there should be any difference. The vptr is just a hidden 
member of the object. Consider:

struct Derived
{
    int * p;
    int q;

    Derived(): p( &q ), q( 5 ) {}
    void DerivedFunc() { std::cout << *p << std::endl; }
};

If the store to global_ptr doesn't have release semantics, 
global_ptr->DerivedFunc() can easily access an uninitialized p and trap. 




More information about the cpp-threads mailing list