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

Создана копия решения без контроля исходного кода.

Загруженный здесь, если вы хотите иметь дело с проблемой.

Изменено использование события ручного сброса в коде для демонстрационных целей и закомментирован код проблемы.

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;

Обновление от MSDN:

Если текущий поток уже имеет блокировку записи, блокировка чтения не получается. Вместо этого увеличивается число блокировок блокировки записи. Это предотвращает блокировку потока собственной блокировкой записи. Результат точно такой же, как вызов AcquireWriterLock и дополнительный звонок ReleaseWriterLock требуется при снятии блокировки писателя.

Я думаю, что это случилось

Ваша блокировка чтения читается в той же теме (Task.StartNew метод использует TaskScheduler.Current свойство) блокировка писателя работает, поэтому она не блокируется, поскольку имеет те же привилегии, что и Task сделать, и получил пустой список. Таким образом, в ваших обстоятельствах вы должны выбрать другой примитив синхронизации.

Другие вопросы по тегам