I have a WCF service where InstanceContextMode
is Single
and ConcurrencyMode
is Multiple
. The aim is to create a cache of values on instantiation, without holding up other service calls not reliant upon the creation of the cache.
This way only methods that attempt to obtain a read lock on _classificationsCacheLock
will need to wait until the value of classificationsCache
is populated (classificationsCacheLock.IsWriterLockHeld = false
).
However the issue is, despite acquiring a write Lock in the task thread, the call WCF continues to service in response to a call into the service method GetFOIRequestClassificationsList()
results in _classificationsCacheLock.IsWriterLockHeld
being false
, when it should be true.
Is this odd behavior with WCF
instancing or is this me fundamentally missing a trick.
I tried both acquiring the write lock within the thread context of the constructor (safe option) and within the context of the spawned task thread (which could introduce a race between WCF
then invoking the call to the GetFOIRequestClassificationsList()
function faster than call to classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
) but both result in classificationsCacheLock.IsWriterLockHeld
being false
despite having prevented any race condition via the use of thread.sleep, staggered appropriately apart in the code blocks of each respective thread.
[ServiceBehavior(Namespace = Namespaces.MyNamespace,
ConcurrencyMode = ConcurrencyMode.Multiple,
InstanceContextMode = InstanceContextMode.Single)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MyService : IMyService
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
private List<string> _classificationsCache;
private ReaderWriterLock _classificationsCacheLock;
public MyService()
{
try
{
_classificationsCacheLock = new ReaderWriterLock();
LoadCache();
}
catch (Exception ex)
{
_logger.Error(ex);
}
}
private void LoadCache()
{
// _classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
Task.Factory.StartNew(() =>
{
try
{
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
});//.ContinueWith((prevTask) =>
//{
// if (_classificationsCacheLock.IsWriterLockHeld)
// _classificationsCacheLock.ReleaseWriterLock();
// });
}
public GetFOIRequestClassificationsList_Response GetFOIRequestClassificationsList()
{
try
{
GetFOIRequestClassificationsList_Response response = new GetFOIRequestClassificationsList_Response();
_classificationsCacheLock.AcquireReaderLock(Timeout.Infinite);
response.Classifications = _classificationsCache;
_classificationsCacheLock.ReleaseReaderLock();
return response;
}
catch (Exception ex)
{
_logger.Error(ex);
if (ex is FaultException)
{
throw;
}
else
throw new FaultException(ex.Message);
}
}
}
EDIT 1
Since a number of suggestions were around uncertainty within the thread pool and how a Task might handle thread affinity, I changed the method to explicitly spawn a new thread
var newThread = new Thread(new ThreadStart(() =>
{
try
{
Thread.Sleep(2000);
Debug.WriteLine(string.Format("LoadCache - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
}));
newThread.IsBackground = true;
newThread.Name = "MyNewThread"
newThread.Start();
The result is still the same. classificationsCacheLock.AcquireReaderLock does not wait / block as it seemingly should.
I also added some diagnostics to check if;
- The thread was in fact the same thread, you cannot expect a R/W to block on the same thread
The instance of the _classificationsCacheLock was identical at all times
public GetFOIRequestClassificationsList_Response GetFOIRequestClassificationsList() { try {
GetFOIRequestClassificationsList_Response response = new GetFOIRequestClassificationsList_Response(); Debug.WriteLine(string.Format("GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode())); Debug.WriteLine(string.Format("GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - {0} ", Thread.CurrentThread.ManagedThreadId)); Thread.Sleep(1000); _classificationsCacheLock.AcquireReaderLock(Timeout.Infinite); //_classificationsCacheMRE.WaitOne(); response.Classifications = _classificationsCache; _classificationsCacheLock.ReleaseReaderLock(); return response; } catch (Exception ex) { _logger.Error(ex); if (ex is FaultException) { throw; } else throw new FaultException(ex.Message); } }
The result was ..
GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - 16265870
GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - 9
LoadCache - _classificationsCacheLock.GetHashCode - 16265870
LoadCache - Thread.CurrentThread.ManagedThreadId- 10
.. in that order so now we have a race condition as expected since the write lock was obtained in the newly created thread. The actual WCF
Service call is being made before the constructor's spawned thread has been scheduled to actually run. So I move
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
to the constructor since this is guaranteed to be executed before any class fields are accessed.
Still the AcquireWriterLock does not block, despite the evidence that the constructor was initialized on a different WCF
thread to the WCF
thread that executes the service method.
private void LoadCache()
{
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
Debug.WriteLine(string.Format("LoadCache constructor thread - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache constructor thread - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
var newThread = new Thread(new ThreadStart(() =>
{
try
{
Thread.Sleep(5000);
Debug.WriteLine(string.Format("LoadCache new thread - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache new thread - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
// _classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
}));
newThread.IsBackground = true;
newThread.Name = "CheckQueues" + DateTime.Now.Ticks.ToString();
newThread.Start();
}
Again AcquireWriterLock does not block and allows a null reference classificationsCache to be assigned.
The result was ..
LoadCache constructor thread - _classificationsCacheLock.GetHashCode - 22863715
LoadCache constructor thread - Thread.CurrentThread.ManagedThreadId- 9
GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - 22863715
GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - 8
LoadCache new thread - _classificationsCacheLock.GetHashCode - 22863715
LoadCache new thread - Thread.CurrentThread.ManagedThreadId- 10
EDIT 2
Created a copy of the solution without source control.
Uploaded here if you want to have hands on with the issue.
Changed to using manual reset event in code for demo purposes and commented out problem code.
- Place your breakpoints
- run Example.Web in Debug
- in the browser navigate to 'http://localhost:11164/GetFOIRequestClassificationsList.htm' and click the button.
MRE works, ReaderWriterLock does not work as expected.
.net 4.0 - C#