Подтвердить что ты не робот

Почему LogicalCallContext не работает с async?

В этом question принятый ответ Стивена Клири говорит, что LogicalCallContext не может корректно работать с async. Он также опубликовал об этом в этом потоке MSDN.

LogicalCallContext сохраняет Hashtable, сохраняя данные, отправленные в CallContext.LogicalGet/SetData. И это только мелкая копия этого Hashtable. Поэтому, если вы храните в нем изменяемый объект, разные задачи/потоки будут видеть друг друга. Вот почему пример программы Стивена Клири (NDC), опубликованный в этом потоке MSDN, работает некорректно.

Но AFAICS, если вы сохраняете неизменяемые данные в Hashtable (возможно, используя неизменяемые коллекции), это должно работать, и давайте внедрить NDC.

Однако, Стивен Клири также сказал в этом принятом ответе:

CallContext не может быть использован для этого. Microsoft специально рекомендуется против использования CallContext для чего угодно, кроме удаленных. Более того, логический CallContext не понимает, как методы async возвращаются раньше и возобновляются позже.

К сожалению, эта ссылка на рекомендацию Microsoft недоступна (страница не найдена). Поэтому мой вопрос: почему это не рекомендуется? Почему я не могу использовать LogicalCallContext таким образом? Что значит сказать, что он не понимает асинхронные методы? Из вызывающего POV они просто методы, возвращающие задачи, нет?

ETA: см. также этот другой вопрос. Там ответ Стивена Клири говорит:

вы можете использовать CallContext.LogicalSetData и CallContext.LogicalGetData, но я рекомендую вам не потому, что они не поддерживают какой-либо "клонирование" при использовании простого parallelism

Это похоже на мое дело. Поэтому я должен был бы создать NDC, который на самом деле нужен мне, а не для log4net.

Я написал несколько примеров кода и, похоже, работает, но простое тестирование не всегда ловит concurrency ошибки. Итак, поскольку в этих других сообщениях есть намеки, что это может не сработать, я все еще спрашиваю: действительно ли этот подход?

ETA: Когда я запускаю Стивена, предлагаю повторить из приведенного ниже ответа), я не получаю неправильных ответов, которые он говорит, что я хочу, я получаю правильные ответы. Даже там, где он сказал: "Значение LogicalCallContext здесь всегда" 1 ", я всегда получаю правильное значение 0. Возможно, это связано с состоянием гонки? Во всяком случае, я все еще не воспроизвел какие-либо проблемы на своем компьютере. Вот точный код, который я запускаю; он печатает только" истину "здесь, где Стивен говорит, что должен печатать" ложь" хотя бы некоторое время.

private static string key2 = "key2";
private static int Storage2 { 
    get { return (int) CallContext.LogicalGetData(key2); } 
    set { CallContext.LogicalSetData(key2, value);} 
}

private static async Task ParentAsync() {
  //Storage = new Stored(0); // Set LogicalCallContext value to "0".
  Storage2 = 0;

  Task childTaskA = ChildAAsync();
  // LogicalCallContext value here is always "1".
  // -- No, I get 0
  Console.WriteLine(Storage2 == 0);

  Task childTaskB = ChildBAsync();
  // LogicalCallContext value here is always "2".
  // -- No, I get 0
  Console.WriteLine(Storage2 == 0);

  await Task.WhenAll(childTaskA, childTaskB);
  // LogicalCallContext value here may be "0" or "1".
  // -- I always get 0
  Console.WriteLine(Storage2 == 0);
}

private static async Task ChildAAsync() {
  var value = Storage2; // Save LogicalCallContext value (always "0").
  Storage2 = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "1" or "2".
  Console.WriteLine(Storage2 == 1);

  Storage2 = value; // Restore original LogicalCallContext value (always "0").
}

private static async Task ChildBAsync() {
  var value = Storage2; // Save LogicalCallContext value (always "1").
  Storage2 = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "0" or "2".
  Console.WriteLine(Storage2 == 2);

  Storage2 = value; // Restore original LogicalCallContext value (always "1").
}

public static void Main(string[] args) {
  try {
    ParentAsync().Wait();
  }
  catch (Exception e) {
    Console.WriteLine(e);
  }

Итак, мой повторный вопрос: что (если что-либо) неправильно с приведенным выше кодом?

Кроме того, когда я смотрю код для CallContext.LogicalSetData, он вызывает Thread.CurrentThread.GetMutableExecutionContext() и модифицирует это. И GetMutableExecutionContext говорит:

if (!this.ExecutionContextBelongsToCurrentScope)
    this.m_ExecutionContext = this.m_ExecutionContext.CreateMutableCopy();
  this.ExecutionContextBelongsToCurrentScope = true;

И CreateMutableCopy в конце концов делает мелкую копию LogicalCallContext Hashtable, которая содержит данные, предоставленные пользователем.

Итак, пытаясь понять, почему этот код не работает для Stephen, это потому, что иногда ExecutionContextBelongsToCurrentScope имеет неправильное значение? Если это так, возможно, мы заметим, когда это произойдет - увидев, что изменился либо текущий идентификатор задачи, либо текущий идентификатор потока, и вручную сохраните отдельные значения в нашей неизменяемой структуре, с ключом с помощью идентификатора thread + task. (С этим подходом возникают проблемы с производительностью, например, сохранение данных для мертвых задач, но помимо этого они будут работать?)

4b9b3361

Ответ 1

Стивен подтверждает, что это работает на .Net 4.5 и Win8/2012. Не тестировалось на других платформах и, как известно, не работало, по крайней мере, с некоторыми из них. Таким образом, ответ заключается в том, что Microsoft объединила свою игру и устранила основную проблему, по крайней мере, в самой последней версии .NET и асинхронного компилятора.

Итак, ответ: он работает, а не на старых версиях .Net. (Таким образом, проект log4net не может использовать его для предоставления общего NDC.)

Ответ 2

Обновление: Этот ответ неверен для .NET 4.5. Подробнее см. мое сообщение в блоге AsyncLocal.

Здесь ситуация (повторяя несколько вопросов в вашем вопросе):

  • LogicalCallContext будет протекать с вызовами async; вы можете использовать его, чтобы установить некоторые неявные данные и прочитать его с помощью метода async далее в стеке вызовов.
  • Все копии LogicalCallContext являются неглубокими копиями, без каких-либо ограничений для кода конечного пользователя для работы с глубоким копированием.
  • Когда вы выполняете "простой parallelism" с async, существует только одна копия LogicalCallContext, разделенная между различными методами async.

LogicalCallContext отлично работает, если ваш код async все линейный:

async Task ParentAsync()
{
  ... = 0; // Set LogicalCallContext value to "0".

  await ChildAAsync();
  // LogicalCallContext value here is always "0".

  await ChildBAsync();
  // LogicalCallContext value here is always "0".
}

async Task ChildAAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here is always "1".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

async Task ChildBAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here is always "2".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

Но все не так хорошо, когда вы используете то, что я называю "простым parallelism" (начиная несколько методов async, а затем используя Task.WaitAll или аналогичный). Это пример, похожий на мой пост форума MSDN (для простоты предположим непараллельный SynchronizationContext, такой как GUI или ASP.NET):

Изменить: комментарии кода неверны; см. комментарии к этому вопросу и ответ

async Task ParentAsync()
{
  ... = 0; // Set LogicalCallContext value to "0".

  Task childTaskA = ChildAAsync();
  // LogicalCallContext value here is always "1".

  Task childTaskB = ChildBAsync();
  // LogicalCallContext value here is always "2".

  await Task.WhenAll(childTaskA, childTaskB);
  // LogicalCallContext value here may be "0" or "1".
}

async Task ChildAAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "1" or "2".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

async Task ChildBAsync()
{
  int value = ...; // Save LogicalCallContext value (always "1").
  ... = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "0" or "2".

  ... = value; // Restore original LogicalCallContext value (always "1").
}

Проблема заключается в том, что LogicalCallContext делится между ParentAsync, ChildAAsync и ChildBAsync, без какого-либо способа подключить или принудительно выполнить операцию глубокой копии. В "линейном" примере контекст также используется совместно, но только один метод был активен одновременно.

Даже если данные, хранящиеся в LogicalCallContext, неизменяемы (как в моем примере с целым числом), вам все равно придется обновить значение LogicalCallContext, чтобы реализовать NDC, а это означает, проблема с копиями будет испорчена.

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

P.S. Стивен Тууб отметил, что рекомендация использовать CallContext только для удаленного использования (которая была дана без причины, IIRC) больше не применяется. Мы можем свободно использовать LogicalCallContext... если мы сможем заставить его работать.;)