c# - Should thread-safe class have a memory barrier at the end of its constructor? -
when implementing class intended thread-safe, should include memory barrier @ end of constructor, in order ensure internal structures have completed being initialized before can accessed? or responsibility of consumer insert memory barrier before making instance available other threads?
simplified question:
is there race hazard in code below give erroneous behaviour due lack of memory barrier between initialization , access of thread-safe class? or should thread-safe class protect against this?
concurrentqueue<int> queue = null; parallel.invoke( () => queue = new concurrentqueue<int>(), () => queue?.enqueue(5));
note acceptable program enqueue nothing, happen if second delegate executes before first. (the null-conditional operator ?.
protects against nullreferenceexception
here.) however, should not acceptable program throw indexoutofrangeexception
, nullreferenceexception
, enqueue 5
multiple times, stuck in infinite loop, or of other weird things caused race hazards on internal structures.
elaborated question:
concretely, imagine implementing simple thread-safe wrapper queue. (i'm aware .net provides concurrentqueue<t>
; example.) write:
public class threadsafequeue<t> { private readonly queue<t> _queue; public threadsafequeue() { _queue = new queue<t>(); // thread.memorybarrier(); // line required? } public void enqueue(t item) { lock (_queue) { _queue.enqueue(item); } } public bool trydequeue(out t item) { lock (_queue) { if (_queue.count == 0) { item = default(t); return false; } item = _queue.dequeue(); return true; } } }
this implementation thread-safe, once initialized. however, if initialization raced consumer thread, race hazards arise, whereby latter thread access instance before internal queue<t>
has been initialized. contrived example:
threadsafequeue<int> queue = null; parallel.for(0, 10000, => { if (i == 0) queue = new threadsafequeue<int>(); else if (i % 2 == 0) queue?.enqueue(i); else { int item = -1; if (queue?.trydequeue(out item) == true) console.writeline(item); } });
it acceptable code above miss numbers; however, without memory barrier, getting nullreferenceexception
(or other weird result) due internal queue<t>
not having been initialized time enqueue
or trydequeue
called.
is responsibility of thread-safe class include memory barrier @ end of constructor, or consumer should include memory barrier between class's instantiation , visibility other threads? convention in .net framework classes marked thread-safe?
edit: advanced threading topic, understand confusion in of comments. instance can appear half-baked if accessed other threads without proper synchronization. topic discussed extensively within context of double-checked locking, broken under ecma cli specification without use of memory barriers (such through volatile
). per jon skeet:
the java memory model doesn't ensure constructor completes before reference new object assigned instance. java memory model underwent reworking version 1.5, double-check locking still broken after without volatile variable (as in c#).
without memory barriers, it's broken in ecma cli specification too. it's possible under .net 2.0 memory model (which stronger ecma spec) it's safe, i'd rather not rely on stronger semantics, if there's doubt safety.
lazy<t>
choice thread-safe initialization. think should left consumer provide that:
var queue = new lazy<threadsafequeue<int>>(() => new threadsafequeue<int>()); parallel.for(0, 10000, => { else if (i % 2 == 0) queue.value.enqueue(i); else { int item = -1; if (queue.value.trydequeue(out item) == true) console.writeline(item); } });
Comments
Post a Comment