Почему LogicalCallContext не работает с асинхронной синхронизацией?
В этом вопросе принятый ответ Стивена Клири говорит, что LogicalCallContext не может корректно работать с async. Он также написал об этом в этой ветке MSDN.
LogicalCallContext хранит Hashtable для хранения данных, отправленных в CallContext.LogicalGet/SetData. И это только мелкая копия этого Hashtable. Поэтому, если вы сохраните в нем изменяемый объект, различные задачи / потоки будут видеть изменения друг друга. Вот почему пример программы NDC Стивена Клири (размещенной в этом потоке MSDN) не работает правильно.
Но AFAICS, если вы храните только неизменяемые данные в Hashtable (возможно, с помощью неизменяемых коллекций), это должно сработать, и давайте реализуем NDC.
Тем не менее, Стивен Клири также сказал в этом принятом ответе:
CallContext не может быть использован для этого. Microsoft особо рекомендовала не использовать CallContext для чего-либо, кроме удаленного взаимодействия. Более того, логический CallContext не понимает, как асинхронные методы возвращаются раньше и возобновляются позже.
К сожалению, ссылка на рекомендацию Microsoft недоступна (страница не найдена). Итак, мой вопрос, почему это не рекомендуется? Почему я не могу использовать LogicalCallContext таким образом? Что значит сказать, что он не понимает асинхронные методы? Из POV вызывающего абонента это просто методы, возвращающие задачи, нет?
ЭТА: см. Также этот другой вопрос. Там ответ Стивена Клири говорит:
Вы могли бы использовать CallContext.LogicalSetData и CallContext.LogicalGetData, но я рекомендую не делать этого, потому что они не поддерживают никакого вида "клонирования", когда вы используете простой параллелизм
Это, кажется, поддерживает мой случай. Так что я должен быть в состоянии построить NDC, который на самом деле то, что мне нужно, но не для log4net.
Я написал пример кода, и он, кажется, работает, но простое тестирование не всегда выявляет ошибки параллелизма. Так как в этих других постах есть подсказки, что это может не сработать, я все еще спрашиваю: является ли этот подход верным?
ETA: Когда я запускаю предложенное Стивеном репродукцию из приведенного ниже ответа), я не получаю неправильных ответов, которые, по его словам, будут, я получаю правильные ответы. Даже там, где он сказал: "Значение LogicalCallContext здесь всегда равно 1", я всегда получаю правильное значение 0. Возможно, это связано с состоянием гонки? Во всяком случае, я до сих пор не воспроизвел никаких реальных проблем на моем собственном компьютере. Вот точный код, который я использую; здесь он печатает только "true", где Стивен говорит, что он должен печатать "false" хотя бы часть времени.
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 в конечном итоге делает поверхностную копию Hashtable LogicalCallContext, который содержит предоставленные пользователем данные.
Итак, пытаясь понять, почему этот код не работает для Стивена, это потому, что ExecutionContextBelongsToCurrentScope иногда имеет неправильное значение? Если это так, может быть, мы можем заметить, когда это произойдет - увидев, что либо текущий идентификатор задачи, либо текущий идентификатор потока изменились - и вручную сохранить отдельные значения в нашей неизменной структуре с ключом потока + идентификатор задачи. (При таком подходе возникают проблемы с производительностью, например, сохранение данных для "мертвых" задач, но кроме этого это сработает?)
2 ответа
Стивен подтверждает, что это работает на.Net 4.5 и Win8/2012. Не тестировался на других платформах и не работал хотя бы на некоторых из них. Таким образом, ответ заключается в том, что Microsoft собрала свою игру и исправила основную проблему по крайней мере в самой последней версии.Net и асинхронном компиляторе.
Поэтому ответ таков: он работает, но не на старых версиях.Net. (Таким образом, проект log4net не может использовать его для предоставления общего NDC.)
Обновление: этот ответ не является правильным для.NET 4.5. Смотрите мой блог на AsyncLocal
для деталей.
Вот ситуация (повторяя несколько пунктов в вашем вопросе):
LogicalCallContext
будет течь сasync
звонки; Вы можете использовать его, чтобы установить некоторые неявные данные и читать их изasync
метод дальше вниз по стеку вызовов.- Все копии
LogicalCallContext
это мелкие копии, без какого-либо способа для кода конечного пользователя подключиться к операции глубокого копирования. - Когда вы делаете "простой параллелизм" с
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").
}
Но вещи не так хороши, когда вы используете то, что я называю "простой параллелизм" (начиная несколько 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, и это означает, что проблема совместного использования без копий испортит ее.
Я подробно рассмотрел это и пришел к выводу, что решение невозможно. Если вы можете понять это, я был бы очень рад оказаться неправым.:)
PS Стивен Туб отметил, что рекомендация использовать CallContext
только для удаленного взаимодействия (которое было предоставлено без причины, IIRC) больше не применяется. Мы можем свободно использовать LogicalCallContext
... если мы сможем заставить его работать.;)