2015-03-12 2 views
9

В зависимости от того, использую ли я код на основе async/await или код на основе TPL, у меня возникают два разных поведения в отношении очистки логического CallContext.Очистка CallContext в TPL

можно установить и ясно, логично CallContext точно так, как я ожидать, если я использую следующий асинхр/ожидают Код:

class Program 
{ 
    static async Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     await Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     return; 
    } 

    static void Main(string[] args) 
    { 
     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 

    } 
} 

выше выдает следующее:

{Place = Task.Run , Id = 9, Msg = мир}
{Place = Main, Id = 8, Msg =}

Обратите внимание на Msg =, который указывает, что CallContext на основной теме был освобожден и пуст.

Но когда я переключаюсь на чистый TPL код/​​TAP Я не могу достичь того же эффекта ...

class Program 
{ 
    static Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     var result = Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     return result; 
    } 

    static void Main(string[] args) 
    { 
     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 
    } 
} 

Вышеприведенные выходов следующее:

{Место = Task.Run , Id = 10, Msg = мир}
{Place = Main, Id = 9, Msg = мир}

Что я могу сделать, чтобы заставить TPL, чтобы "освободить" логический CallContext так же, как и код async/await?

Меня не интересует альтернатива CallContext.

Я надеюсь, что указанный выше код TPL/TAP будет исправлен, чтобы я мог использовать его в проектах, ориентированных на инфраструктуру .NET. Если это невозможно в .net 4.0, мне все же интересно, если это можно сделать в .net 4.5.

ответ

8

В async методы CallContext копируется на записи:

При запуске метода асинхронного , он уведомляет свой контекст логического вызова, чтобы активировать поведение копирования на запись. Это означает, что текущий контекст логического вызова фактически не изменен, но он отмечен так, что если ваш код вызывает CallContext.LogicalSetData, данные контекста логического вызова копируются в новый текущий контекст логического вызова до его изменения.

От Implicit Async Context ("AsyncLocal")

Это означает, что в вашей версии asyncCallContext.FreeNamedDataSlot("hello") продолжение является излишним, как и без него:

static async Task DoSomething() 
{ 
    CallContext.LogicalSetData("hello", "world"); 

    await Task.Run(() => 
     Console.WriteLine(new 
     { 
      Place = "Task.Run", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     })); 
} 

CallContext в Main не будет содержать слот "hello":

{Place = Task.Run, Id = 3, Msg = мир}
{Place = Main, Id = 1, Msg =}

В эквиваленте TPL весь код вне Task.Run (который должен быть Task.Factory.StartNew как Task.Run был добавлен в .Net 4.5) работает на одной и той же резьбе с точно такой же. Если вы хотите, чтобы очистить его, вы должны сделать это в этом контексте (а не в продолжении):

static Task DoSomething() 
{ 
    CallContext.LogicalSetData("hello", "world"); 

    var result = Task.Factory.StartNew(() => 
     Debug.WriteLine(new 
     { 
      Place = "Task.Run", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     })); 

    CallContext.FreeNamedDataSlot("hello"); 
    return result; 
} 

Вы можете даже отрывать сфера из нее, чтобы убедиться, что вы всегда убирать за собой:

static Task DoSomething() 
{ 
    using (CallContextScope.Start("hello", "world")) 
    { 
     return Task.Factory.StartNew(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })); 
    } 
} 

Использование:

public static class CallContextScope 
{ 
    public static IDisposable Start(string name, object data) 
    { 
     CallContext.LogicalSetData(name, data); 
     return new Cleaner(name); 
    } 

    private class Cleaner : IDisposable 
    { 
     private readonly string _name; 
     private bool _isDisposed; 

     public Cleaner(string name) 
     { 
      _name = name; 
     } 

     public void Dispose() 
     { 
      if (_isDisposed) 
      { 
       return; 
      } 

      CallContext.FreeNamedDataSlot(_name); 
      _isDisposed = true; 
     } 
    } 
} 
+0

в вашей TPL версии, существует риск того, что логический CallContext будет освобожден до Task.Factory.StartNew имел шанс захватить его? Я также должен быть уверен, что все продолжения (если они есть) из Task.Factory.StartNew действительно имеют CallContext, даже если он был «освобожден» основным потоком. –

+0

@BrentArias вы можете протестировать его с помощью Thread.Sleep (я сделал). Task.Factory.StartNew as do Task.Run захватывает (копирует) контекст и сохраняет его в Задаче, поэтому вам не нужно беспокоиться об этом. У вас больше информации об этом здесь: http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx – i3arnon

+0

@BrentArias * ", когда вы используете Task.Run, вызов Run захватывает ExecutionContext из вызывающего потока, сохраняя этот экземпляр ExecutionContext в объекте Task. Когда делегат, предоставленный Task.Run, позже вызывается как часть выполнения этой задачи, это делается через ExecutionContext.Run, используя сохраненный контекст. Это верно для Task.Run, для ThreadPool.QueueUserWorkItem, для Delegate.BeginInvoke для Stream.BeginRead, для DispatcherSynchronizationContext.Post и для любого другого асинхронного API, о котором вы можете думать. "* – i3arnon

4

Хороший вопрос. Версия await может не работать так, как вы думаете, она здесь. Давайте добавим еще одну строку протоколирования внутри DoSomething:

class Program 
{ 
    static async Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     await Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     Debug.WriteLine(new 
     { 
      Place = "after await", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 
    } 

    static void Main(string[] args) 
    { 

     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 

     Console.ReadLine(); 
    } 
} 

Выход:

 
{ Place = Task.Run, Id = 10, Msg = world } 
{ Place = after await, Id = 11, Msg = world } 
{ Place = Main, Id = 9, Msg = } 

Обратите внимание на "world" все еще там после await, потому что там было раньше await. И его нет после DoSomething().Wait(), потому что его там не было, в первую очередь.

Интересно, что версия DoSomethingasync создает клон копирования при записи в LogicalCallContext для его объема, при первом LogicalSetData. Это происходит даже тогда, когда внутри него нет асинхронности - попробуйте await Task.FromResult(0). Я предполагаю, что весь ExecutionContext будет клонирован для области метода async после первой операции записи.

Ото, для версии без асинхронном нет «логической» сферы применения и не внешнего ExecutionContext здесь, так что клон копирования при записи из ExecutionContext становится текущим для Main нити (но продолжения и Task.Run лямбды еще получить свои собственные клоны). Таким образом, вы либо должны двигаться CallContext.LogicalSetData("hello", "world") внутри Task.Run лямбды, или клонировать контекст вручную:

static Task DoSomething() 
{ 
    var ec = ExecutionContext.Capture(); 
    Task task = null; 
    ExecutionContext.Run(ec, _ => 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     var result = Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     task = result; 
    }, null); 

    return task; 
} 
Смежные вопросы