2017-01-30 10 views
4

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

Пример

У меня есть долгоиграющий, массивно параллельная служба работника, который имеет ровно один случай ошибки - когда он не может восстановить работу. Степень параллелизма (количество используемых концептуальных потоков) настраивается. Обратите внимание: концептуальные потоки реализованы как задачи <> через TPL. Поскольку служба постоянно работает, пытаясь получить работу при умножении на неизвестную степень параллелизма, это может означать, что от тысячи до десятков тысяч ошибок могут быть сгенерированы в секунду.

Таким образом, мне нужен механизм отчетности, который сроки, а не попытку переплете, который изолирован на свою собственную концептуальную нить, и это сократимое. С этой целью я разработал рекурсивную задач лямбда, который обращается мой счетчик ошибок каждые 5 минут за пределами первичной попытки на основе зацикливания, который пытается получить работу:

var faults = 1; 
Action<Task> reportDelay = null; 
reportDelay = 
    // 300000 is 5 min 
    task => Task.Delay(300000, cancellationToken).ContinueWith(
     subsequentTask => 
     { 
      // `faults` is modified outside the anon method 
      Logger.Error(
       $"{faults} failed attempts to get work since the last known success."); 
      reportDelay(subsequentTask); 
     }, 
     cancellationToken); 

// start the report task - runs concurrently with below 
reportDelay.Invoke(Task.CompletedTask); 

// example get work loop for context 
while (true) 
{ 
    object work = null; 
    try 
    { 
     work = await GetWork(); 
     cancellationToken.Cancel(); 
     return work; 
    } 
    catch 
    { 
     faults++; 
    }   
} 

Опасения по

Я понимаю, что, в этом случае сгенерированное замыкание с точкой по ссылке на мою переменную faults (которая увеличивается, когда какой-либо концептуальный поток пытается получить работу, но не может). Я также понимаю, что это обычно обескураживает, но из того, что я могу сказать, только потому, что это приводит к неожиданному поведению, когда кодируется, ожидая закрытия, чтобы зафиксировать значение.

Здесь я хочу и полагаюсь на замыкание, фиксируя переменную faults по ссылке. Я хочу сообщить значение переменной во время вызова продолжения (это не обязательно должно быть точным). Я слегка обеспокоен тем, что faults преждевременно GC'd, но я отменяю цикл, прежде чем выйти из этого лексического масштаба, заставляя меня думать, что он должен быть безопасным. Есть ли что-нибудь еще, о чем я не думаю? Какие опасности существуют при рассмотрении доступа к закрытию за пределами изменчивости базовой стоимости?

Ответ и объяснение

Я приняли ответ ниже, refactors кода, чтобы избежать необходимости доступа замыкания пути материализации монитора ошибок в свой класс. Однако, поскольку это не отвечает на вопрос напрямую, я буду включать краткое объяснение здесь для будущих читателей о надежном поведении:

До тех пор, пока закрытая переменная остается в пределах срока действия крышки, она можно полагаться на то, чтобы вести себя как истинная ссылочная переменная. Опасности доступа к переменной модифицированную во внешней области видимости изнутри крышки являются:

  • Вы должны понимать, что переменная будет вести себя в качестве эталона в рамках закрытия, мутирует его значение, как он изменяется во внешнем объеме ,Переключатель закрытия всегда будет содержать текущее значение времени выполнения внешней переменной области видимости, не значение в момент создания замыкания.
  • Вы должны написать свою программу таким образом, чтобы гарантировать, что время жизни внешней переменной будет таким же или больше, чем сама анонимная функция/закрытие. Если вы мусор собираете внешнюю переменную, то ссылка станет недопустимым указателем.
+0

Почему вы создаете анонимную функцию только для ее вызова в первую очередь? Просто запустите код, не используя анонимную функцию. – Servy

+0

Поскольку a) его цикл так «вызывается» один раз, но работает непрерывно до отмены, и b) без обращения к этой переменной «ошибка» механизм отчета бесполезен. Поскольку первичный цикл является _attempt-bound_ (т. Е. Он зацикливается при попытке выполнить попытку), цикл отчета должен быть каким-то независимым (в противном случае он также будет связан с попытками), а также требует ссылки на эту переменную, чтобы включить его в Ошибка. –

+0

@Servy, он называет эту функцию рекурсивно, а не только один раз. – Evk

ответ

1

Ниже приводится краткая альтернатива, позволяющая избежать некоторых проблем, которые могут возникнуть. Кроме того, как сказал @Servy, просто вызов функции sperate async будет делать. ConcurrentStack просто упрощает добавление и очистку, кроме того, может быть записана дополнительная информация, а не только счет.

public class FaultCounter { 

    private ConcurrentStack<Exception> faultsSinceLastSuccess;   

    public async void RunServiceCommand() { 
     faultsSinceLastSuccess = new ConcurrentStack<Exception>(); 
     var faultCounter = StartFaultLogging(new CancellationTokenSource()); 
     var worker = DoWork(new CancellationTokenSource()); 
     await Task.WhenAll(faultCounter, worker); 
     Console.WriteLine("Done."); 
    } 

    public async Task StartFaultLogging(CancellationTokenSource cts) { 
     while (true && !cts.IsCancellationRequested) { 
      Logger.Error($"{faultsSinceLastSuccess.Count} failed attempts to get work since the last known success."); 
      faultsSinceLastSuccess.Clear(); 
      await Task.Delay(300 * 1000); 
     } 
    } 

    public async Task<object> DoWork(CancellationTokenSource cts) {    
     while (true) { 
      object work = null; 
      try { 
       work = await GetWork(); 
       cts.Cancel(); 
       return work; 
      } 
      catch (Exception ex) { 
       faultsSinceLastSuccess.Push(ex); 
      } 
     } 
    } 
} 
+0

Мне нравится эта концепция. Повторное включение FaultCounter в свой собственный класс, по-видимому, устраняет необходимость в анонимном методе, но я обеспокоен давлением памяти, который может возникнуть в моей ситуации. Пойдет по этой дороге и посмотрю, не могу ли я сделать эту работу. –

+1

Я принял это как ответ, потому что это проспект, в котором я преследовал. Казалось бы, лучше не полагаться на модифицированный доступ к закрытию ссылкой только потому, что он ведет себя против интуиции. Однако это действительно не отвечает на вопрос «могу ли я полагаться на модифицированный доступ к закрытию, чтобы вести себя так, как ожидалось?» Этот ответ, во всех смыслах и целях, да, пока вы ожидаете, что он будет вести себя как ссылка, а не значение. Я изменю исходное сообщение, чтобы уточнить, что так вы получаете ответы, но будущие читатели не оставляют удивляться. –

0

Я вижу некоторые проблемы здесь, в вашем решении:

  1. Вы считывание/запись faults значение переменной в не потокобезопасного образом, так что в теории любой из ваших потоков может использовать его старое значение , Вы можете исправить это с использованием класса Interlocked, особенно для приращения.
  2. Ваше действие не похоже на работу с параметром task, поэтому зачем вам это нужно, как Action Принимать задание? Кроме того, в продолжении вы не проверяете флаг отмены токена, поэтому, теоретически, вы можете получить ситуацию, когда ваш код работает гладко, но вы все равно получаете сообщения об ошибках.
  3. Вы запускаете длинную задачу без long-running flag, что нецелесообразно для планировщика задач.
  4. Ваше рекурсивное действие может быть переписано в while, вместо этого удалите ненужные служебные данные в коде.

Затворы в C# являются implemented into a compiler generated class, поэтому GC не должно быть проблемой для вас, до тех пор, пока вы зацикливание код повторных попыток.

+0

1. Да, я знаю о блокированной проблеме. ATM это ограничивается выполнением в одном потоке, поэтому это не проблема, но если я когда-нибудь перейду к многопоточности повтора, тогда мне придется обратиться к этому. 2. Действие принимает задачу для включения цикла. См. 'ReportDelay (afterTask)'. 3. Да, код является грубым, и в моей завершенной реализации будет использоваться LongRunning 4. Вы правы. Я думаю, что решение состоит в том, чтобы реактивировать логику повтора в свой собственный объект и написать отдельные методы асинхронного цикла. –

+0

> 2. Действие требует выполнения цикла. См. ReportDelay (afterTask) - требуется 'afterTask', а не' task' – VMAtm

+0

. Я понимаю, что вы имеете в виду. Да, это может быть просто «Action reportDelay», а затем вызвать «reportDelay()» для инициализации хвостовой рекурсии. Как я уже сказал, грубый код :) –