2015-04-24 8 views
3

У меня есть сложная проблема, возникающая в некоторых моих кодах. У меня есть менеджер кэша, который либо возвращает элементы из кеша, либо вызывает делегат для их создания (дорого).В функции возврата доходности можно гарантировать, что финализатор вызывается в том же потоке?

Я обнаружил, что у меня проблемы с завершающей частью моего метода, выполняемой в другом потоке, чем остальные.

Вот урезанная версия

public IEnumerable<Tuple<string, T>> CacheGetBatchT<T>(IEnumerable<string> ids, BatchFuncT<T> factory_fn) where T : class 
    { 

     Dictionary<string, LockPoolItem> missing = new Dictionary<string, LockPoolItem>(); 

     try 
     { 
      foreach (string id in ids.Distinct()) 
      { 
       LockPoolItem lk = AcquireLock(id); 
       T item; 

       item = (T)resCache.GetData(id); // try and get from cache 
       if (item != null) 
       { 
        ReleaseLock(lk); 
        yield return new Tuple<string, T>(id, item); 
       } 
       else 
        missing.Add(id, lk);      
      } 

      foreach (Tuple<string, T> i in factory_fn(missing.Keys.ToList())) 
      { 
       resCache.Add(i.Item1, i.Item2); 
       yield return i; 
      } 

      yield break;      // why is this needed? 
     } 
     finally 
     { 
      foreach (string s in missing.Keys) 
      { 
       ReleaseLock(l); 
      } 
     } 
    } 

Приобретать и снятия блокировки заполнить словарь с объектами LockPoolItem, которые были заблокированы с Monitor.Enter/Monitor.Exit [Я также попытался мьютексы]. Проблема возникает, когда ReleaseLock вызывается в другом потоке, из которого был вызван AcquireLock.

Проблема возникает, когда вы вызываете это из другой функции, которая использует потоки, иногда вызываемый блок finalize вызывается из-за удаления IEnumerator, выполняющегося на возвращенной итерации.

Следующий пример представляет собой простой пример.

BlockingCollection<Tuple<Guid, int>> c = new BlockingCollection<Tuple<Guid,int>>(); 

      using (IEnumerator<Tuple<Guid, int>> iter = global.NarrowItemResultRepository.Narrow_GetCount_Batch(userData.NarrowItems, dicId2Nar.Values).GetEnumerator()) { 
       Task.Factory.StartNew(() => { 

        while (iter.MoveNext()) { 
         c.Add(iter.Current); 
        } 
        c.CompleteAdding(); 
       }); 
      } 

Это, кажется, не происходит, когда я добавляю перерыв выход - однако я нахожу это трудно отлаживать, как это происходит только очень редко. Тем не менее, это происходит - я пробовал регистрировать идентификаторы потоков и финализировать, если вызывается на разные потоки ...

Я уверен, что это неправильное поведение: я не понимаю, почему метод dispose (т.е. выход с использованием) будет вызван в другой поток.

Любые идеи, как защититься от этого?

+0

Я хотел бы предложить, что любой проект, где вы держите замки в точке, в которой вы доходность сломанный один - вы не имеете ни малейшего представления о том, как долго это будет перед вашим абонентом следующих вызовов 'MoveNext' или, действительно, как вы нашли, «Dispose». Не зная больше о вашей конкретной проблеме, сложно предложить конкретные советы, но я буду искать - измените дизайн, чтобы вы не пощадили своих абонентов, когда вы отпускаете замки. –

+0

Это справедливая точка, однако она не отвечает на вопрос. То, что я пытаюсь достичь, состоит в том, чтобы вернуть поставщику элементы, поскольку они извлекаются из медленного хранилища - некоторые элементы могут занимать секунды, другие миллисекунды, но нет способа узнать, перед какими из них в партии будет медленно возвращаться. Я подозреваю, что мне будет лучше иметь блокирующую коллекцию, предоставляемую функции кеша, и заполнить ее там. Однако я до сих пор не понимаю, почему dispose/finalize вызывается в другом потоке. – dominicbeesley

+0

Нет, это не так, поэтому почему оно опубликовано как комментарий. Если вы готовы внести такие изменения, я могу приложить некоторые усилия и показать вам, как может выглядеть альтернатива.Если вы проверите обсуждение, которое у меня было после ответа supercat, вы увидите, что я несколько дней спорил о том, что нет гарантии, что перечислимое будет возобновлено в одном потоке (особенно если код вызова использует современные функции, такие как ' async') –

ответ

1

Кажется, здесь есть гонка.

Похоже, ваш код вызова создает перечислитель, затем запускает задачу в пуле потоков, чтобы перечислить его, а затем удаляет перечислитель. Мои первоначальные мысли:

  • Если перечислитель расположен до начала перечисления, ничего не произойдет. Из краткого теста это не предотвращает перечисление после его размещения.

  • Если перечислитель расположен во время перечисления, будет вызываться блок finally (в вызывающем потоке), и перечисление остановится.

  • Если перечисление завершено действием задачи, будет вызываться блок finally (в потоке пула потоков).

Чтобы попытаться продемонстрировать, рассмотрим этот метод:

private static IEnumerable<int> Items() 
{    
    try 
    { 
     Console.WriteLine("Before 0"); 

     yield return 0; 

     Console.WriteLine("Before 1"); 

     yield return 1; 

     Console.WriteLine("After 1"); 
    } 
    finally 
    { 
     Console.WriteLine("Finally"); 
    } 
} 

Если вы располагаете, прежде чем перечислять, ничего не будет записана в консоль.Это то, что я подозреваю, что вы будете делать большую часть времени, так как текущий поток достигает конца using блока перед началом задачи:

var enumerator = Items().GetEnumerator(); 
enumerator.Dispose();  

Если перечисление завершается до Dispose, окончательный вызов MoveNext будет вызывать блок finally.

var enumerator = Items().GetEnumerator(); 
enumerator.MoveNext(); 
enumerator.MoveNext(); 
enumerator.MoveNext(); 

Результат:

"Before 0" 
"Before 1" 
"After 1" 
"Finally" 

Если вы располагаете при перечислении, вызов Dispose будет вызывать finally блок:

var enumerator = Items().GetEnumerator(); 
enumerator.MoveNext(); 
enumerator.Dispose(); 

Результат:

"Before 0" 
"Finally" 

Я предлагаю вам создать, перечислить и удалить перечислитель в том же потоке.

+0

Но я думал, что Dispose должен быть вызван в конце блока «using» - на внешнем потоке. Вы говорите, что .MoveNext() может косвенно вызвать Dispose? Я предполагал, что Dispose вызывается в точке, где закрытие} используемого блока есть, то есть на исходном внешнем потоке. – dominicbeesley

+0

'Dispose' будет вызываться в конце блока' using', проблема в том, что выполнение, вероятно, достигнет этой точки до того, как код в 'Task' даже начал выполняться. 'MoveNext' может вызывать блок' finally' в результате того, что ему больше нечего делать, иначе вызов 'Dispose' будет вызван, пока он не будет завершен. –

+0

@dominicbeesley Я обновил ответ, надеюсь, уточнить. –

-1

Термин «финализатор» относится к понятию, совершенно не связанному с «последним» блоком; не гарантируется ничто, связанное с контекстом финализаторов, но я думаю, что вас действительно интересуют «окончательные» блоки.

Блок finally, окруженный yield return, будет выполнен любым потоком, который вызывает Dispose в перечислителе итератора. Перечислители, как правило, имеют право предположить, что все выполненные на них операции, в том числе Dispose, будут выполняться тем же потоком, который их создал, и, как правило, не обязаны вести себя ни в чем, даже отдаленно напоминающем разумную моду, в тех случаях, когда они не являются. Система не мешает коду использовать счетчики в нескольких потоках, но в том случае, если программа использует из нескольких потоков, перечислитель, который не дает никаких обещаний, что он будет работать в этом отношении, означает, что любые последствия, вытекающие из этого, не являются ошибкой перечислителя , а скорее по вине программы, которая использовала его незаконно.

Как правило, для классов достаточно использовать достаточную защиту от недействительной многопоточности, чтобы гарантировать, что ненадлежащее многопоточное использование не приведет к уязвимостям системы безопасности, но не стоит беспокоиться о предотвращении любого другого вида вреда или путаницы.

+0

Я бы поставил под сомнение ваши предположения. Есть ли у вас какие-либо конкретные рекомендации или документация для их резервного копирования? Почему вы утверждаете, что счетчикам разрешено принимать доступ из одного потока? –

+0

То, о чем я сейчас думаю, это, в частности, методы «async» - нет никакой гарантии (в общем), что весь код внутри этого метода будет выполняться в одном потоке, и все же я не знаю о * любом * руководстве что «не использовать' foreach' в асинхронных методах » –

+0

@Damien_The_Unbeliever: использование' foreach' целиком в рамках метода async было бы, по моему пониманию, вызывать 'GetEnumerator' для вызова из неизвестного контекста потоков, но приведет к всем последующим действиям на этом счетчике, который должен выполняться в том же контексте потоков. – supercat

0

Благодаря всем ответам я понял, что происходит и почему. Решение моей проблемы было довольно простым. Я просто должен был убедиться, что все было вызвано в той же теме.

 BlockingCollection<Tuple<Guid, int>> c = new BlockingCollection<Tuple<Guid,int>>(); 

     Task.Factory.StartNew(() => { 
      using (IEnumerator<Tuple<Guid, int>> iter = global.NarrowItemResultRepository.Narrow_GetCount_Batch(userData.NarrowItems, dicId2Nar.Values).GetEnumerator()) { 

       while (iter.MoveNext()) { 
        c.Add(iter.Current); 
       } 
       c.CompleteAdding(); 
      } 
     }); 

 Смежные вопросы

  • Нет связанных вопросов^_^