ReaderWriterLock не работает в конструкторе ServiceBehavior
У меня есть служба WCF, где InstanceContextMode
является Single
а также ConcurrencyMode
является Multiple
, Цель состоит в том, чтобы создать кэш значений при создании экземпляра, не задерживая другие вызовы служб, не зависящие от создания кеша.
Таким образом, только методы, которые пытаются получить блокировку чтения _classificationsCacheLock
нужно будет подождать, пока значение classificationsCache
заселена (classificationsCacheLock.IsWriterLockHeld = false
).
Однако проблема заключается в том, что, несмотря на получение блокировки записи в потоке задач, вызов WCF продолжает обслуживаться в ответ на вызов в метод обслуживания. GetFOIRequestClassificationsList()
результаты в _classificationsCacheLock.IsWriterLockHeld
являющийся false
, когда это должно быть правдой.
Это странное поведение с WCF
например, или это мне принципиально не хватает подвоха.
Я пытался как получить блокировку записи в контексте потока конструктора (безопасный вариант), так и в контексте порожденного потока задач (что могло привести к гонке между WCF
затем вызывая вызов GetFOIRequestClassificationsList()
функционировать быстрее, чем вызов classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
) но оба результата в classificationsCacheLock.IsWriterLockHeld
являющийся false
несмотря на то, что он предотвращал любое состояние гонки с помощью thread.sleep, он смещался соответствующим образом в кодовых блоках каждого соответствующего потока.
[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);
}
}
}
РЕДАКТИРОВАТЬ 1
Поскольку ряд предложений касался неопределенности в пуле потоков и того, как Задача может обрабатывать сходство потоков, я изменил метод, чтобы явно порождать новый поток
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();
Результат все тот же. classificationCacheLock.AcquireReaderLock не ожидает / блокирует, как это должно было бы выглядеть.
Я также добавил немного диагностики, чтобы проверить, если;
- Поток был фактически тем же потоком, вы не можете ожидать, что R/W будет блокироваться в том же потоке
Экземпляр _classificationCacheLock всегда был идентичен
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); } }
Результат был..
GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - 16265870
GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - 9
LoadCache - _classificationsCacheLock.GetHashCode - 16265870
LoadCache - Thread.CurrentThread.ManagedThreadId- 10
.. в таком порядке, так что теперь у нас есть условие гонки, как и ожидалось, поскольку блокировка записи была получена во вновь созданном потоке. Настоящий WCF
Вызов службы выполняется до того, как запланированный запуск потока, созданного конструктором. Итак, я двигаюсь
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
в конструктор, так как это гарантированно будет выполнено до того, как какие-либо поля класса будут доступны.
Тем не менее AcquireWriterLock не блокируется, несмотря на свидетельство того, что конструктор был инициализирован на другом WCF
нить к WCF
поток, который выполняет сервисный метод.
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();
}
Снова AcquireWriterLock не блокирует и позволяет присваивать пустой ссылочный классификационный кэш.
Результат был..
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
РЕДАКТИРОВАТЬ 2
Создана копия решения без контроля исходного кода.
Загруженный здесь, если вы хотите иметь дело с проблемой.
Изменено использование события ручного сброса в коде для демонстрационных целей и закомментирован код проблемы.
- Разместите ваши точки останова
- запустите Example.Web в Debug
- в браузере перейдите к " http://localhost:11164/GetFOIRequestClassificationsList.htm" и нажмите кнопку.
MRE работает, ReaderWriterLock не работает должным образом.
.net 4.0 - C#
1 ответ
в CaseWork()
метод, который вы создаете новый ReaderWriterLock
каждый раз, когда метод вызывается. Таким образом, проверяемый замок просто выходит к сборщику мусора, и возникает новый. Поэтому никакой замок на самом деле правильно добывается.
Почему ты не используешь static
заблокировать это, создав его в static
конструктор?
Поправьте меня, если я ошибаюсь, но я не могу, если вы обновляете свой кеш. Если это правда, я предлагаю вам просто использовать Lazy<T>
учебный класс. Он ориентирован на многопотоковое исполнение и содержит все считыватели до того, как будет установлено значение Он внутренне использует TPL
и просто использовать:
private Lazy<List<string>> _classificationsCache = new Lazy<List<string>>
(() =>
{
var cases = SomeServices.GetAllFOIRequests();
return cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
});
Вы можете получить значение следующим образом:
response.Classifications = _classificationsCache.Value;
Если текущий поток уже имеет блокировку записи, блокировка чтения не получается. Вместо этого увеличивается число блокировок блокировки записи. Это предотвращает блокировку потока собственной блокировкой записи. Результат точно такой же, как вызов
AcquireWriterLock
и дополнительный звонокReleaseWriterLock
требуется при снятии блокировки писателя.
Я думаю, что это случилось
Ваша блокировка чтения читается в той же теме (Task.StartNew
метод использует TaskScheduler.Current
свойство) блокировка писателя работает, поэтому она не блокируется, поскольку имеет те же привилегии, что и Task
сделать, и получил пустой список. Таким образом, в ваших обстоятельствах вы должны выбрать другой примитив синхронизации.