M
Marcel Müller
I have a function that dispatches I/O Tasks while operation on transient
data. There is a deadlock from time to time (about every 3 weeks in
production environment).
Analysis of (forced) crash dumps show that many threads are waiting for
a peding object to be read but no one seems to load the data from the
backend.
Rough pattern:
public IEnumerable<E> GetObjects<E>(IEnumerable<Ref<E>> orefs)
where E : StrongEntityBase<E>
E is some object of an arbitrary entity while Ref<E> is only a
lightwight reference. The function retrieves the requested objects.
Implementation:
var notcached = new List<E>(READPACKAGESIZE);
var waitworker = new WaitForObjectState<E>();
try
{ // retrieve all objects from the cache or create new ones in the cache.
// The new ones are identified by their state and need to be loaded
// before they are returned.
// The call does not block on I/O.
var source = StrongEntityBase<E>.CacheWorker.LookupOrCreate(orefs);
foreach (E obj in source)
{retry:
switch (obj.ObjectState)
{case ObjectState.Virgin: // new object
//
if (!obj.SetPendingState())
goto retry; // another thread already set the state to Pending
notcached.Add(obj);
if (notcached.Count >= READPACKAGESIZE)
{ // Read data from backend. This function blocks.
DoReadPackage(notcached);
foreach (E obj2 in notcached)
yield return obj2;
notcached.Clear();
}
break;
case ObjectState.Pending: // in work by another thread
// wait on obj for the next state change.
waitworker.WaitForObject(obj);
goto retry;
case ObjectState.Valid: // Cached
// cache hit
yield return obj;
}
}
// return remaining objects in last package
// Read data from backend. This function blocks.
DoReadPackage<E>(notcached);
foreach (E obj2 in notcached)
yield return obj2;
notcached.Clear();
}
finally
{ waitworker.Dispose();
// read last package in case of incomplete enumeration because
// another thread could wait on that data.
// in case of exceptions some objects could already be loaded.
notcached.RemoveAll(obj => obj.ObjectState != ObjectState.Pending);
// Read the remaining objects.
DoReadPackage<E>(notcached);
}
The critical point is in my opinion the finally block. If it is not
executed then objects might be left in the cache in state Pending
without any thread going to read them.
So the question is whether finally in a function with yield return is
reliable?
What happens if some code misses to dispose the enumerator returned by
the class generated from yield return?
I do not think that there is a race condition while accessing
ObjectState. ObjectState is only changed by Interlocked.CompareExchange
and it follows a directed graph.
All objects (and their children) are immutable as soon as they are in
state Valid. Before state Valid they are owned by the thread that
changed the state to Pending. This thread is responsible for turning the
state to Valid. The above code should ensure that.
The function DoReadPackage never returns until all objects are in state
Valid. This is proven.
Marcel
data. There is a deadlock from time to time (about every 3 weeks in
production environment).
Analysis of (forced) crash dumps show that many threads are waiting for
a peding object to be read but no one seems to load the data from the
backend.
Rough pattern:
public IEnumerable<E> GetObjects<E>(IEnumerable<Ref<E>> orefs)
where E : StrongEntityBase<E>
E is some object of an arbitrary entity while Ref<E> is only a
lightwight reference. The function retrieves the requested objects.
Implementation:
var notcached = new List<E>(READPACKAGESIZE);
var waitworker = new WaitForObjectState<E>();
try
{ // retrieve all objects from the cache or create new ones in the cache.
// The new ones are identified by their state and need to be loaded
// before they are returned.
// The call does not block on I/O.
var source = StrongEntityBase<E>.CacheWorker.LookupOrCreate(orefs);
foreach (E obj in source)
{retry:
switch (obj.ObjectState)
{case ObjectState.Virgin: // new object
//
if (!obj.SetPendingState())
goto retry; // another thread already set the state to Pending
notcached.Add(obj);
if (notcached.Count >= READPACKAGESIZE)
{ // Read data from backend. This function blocks.
DoReadPackage(notcached);
foreach (E obj2 in notcached)
yield return obj2;
notcached.Clear();
}
break;
case ObjectState.Pending: // in work by another thread
// wait on obj for the next state change.
waitworker.WaitForObject(obj);
goto retry;
case ObjectState.Valid: // Cached
// cache hit
yield return obj;
}
}
// return remaining objects in last package
// Read data from backend. This function blocks.
DoReadPackage<E>(notcached);
foreach (E obj2 in notcached)
yield return obj2;
notcached.Clear();
}
finally
{ waitworker.Dispose();
// read last package in case of incomplete enumeration because
// another thread could wait on that data.
// in case of exceptions some objects could already be loaded.
notcached.RemoveAll(obj => obj.ObjectState != ObjectState.Pending);
// Read the remaining objects.
DoReadPackage<E>(notcached);
}
The critical point is in my opinion the finally block. If it is not
executed then objects might be left in the cache in state Pending
without any thread going to read them.
So the question is whether finally in a function with yield return is
reliable?
What happens if some code misses to dispose the enumerator returned by
the class generated from yield return?
I do not think that there is a race condition while accessing
ObjectState. ObjectState is only changed by Interlocked.CompareExchange
and it follows a directed graph.
All objects (and their children) are immutable as soon as they are in
state Valid. Before state Valid they are owned by the thread that
changed the state to Pending. This thread is responsible for turning the
state to Valid. The above code should ensure that.
The function DoReadPackage never returns until all objects are in state
Valid. This is proven.
Marcel