2015-03-12 13 views
9

W zależności od tego, czy korzystam z kodu asynchronicznego/opartego na oczekiwaniach, czy kodu opartego na licencji TPL, otrzymuję dwa różne zachowania dotyczące czyszczenia logicznego CallContext.Czyszczenie CallContext w języku TPL

mogę ustawić i jasne, logiczne CallContext dokładnie tak, jak ja się spodziewać, jeśli mogę użyć następującego asynchronicznie/czekają Kod:

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") 
     }); 

    } 
} 

Powyższy wyprowadza następujące:

{Place = Task.Run , J = 9, MSG = świecie}
{miejsce = Menem, J = 8, MSG =}

Zawiadomienie Msg = co oznacza, że ​​CallContext na głównym wątku zostało zwolnione i jest puste.

Ale kiedy przełączyć się na czystej OC/code TAP nie mogę osiągnąć ten sam efekt ...

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") 
     }); 
    } 
} 

Powyższe wyjść następuje:

{Place = Task.Run Id = 10, Msg = świat}
{Place = Main, id = 9, Msg = świat}

Czy mogę coś zrobić, aby zmusić do OC "wolne" na logiczny CallContext w taki sam sposób jak robi to kod async/await? Nie jestem zainteresowany alternatywami do CallContext.

Mam nadzieję, że uda mi się naprawić powyższy kod TPL/TAP, aby móc go używać w projektach ukierunkowanych na platformę .net 4.0. Jeśli nie jest to możliwe w .net 4.0, nadal jestem ciekawy, czy można to zrobić w .net 4.5.

Odpowiedz

8

W metodzie CallContextasync jest kopiowany na napisz:

Kiedy zaczyna metoda asynchroniczny , powiadamia swój logiczny kontekst wywołania, aby aktywować zachowanie kopiowania przy zapisie. Oznacza to, że bieżący kontekst wywołania logicznego nie został faktycznie zmieniony, ale jest oznaczony tak, że jeśli twój kod wywołuje CallContext.LogicalSetData, dane kontekstu połączenia logicznego są kopiowane do nowego kontekstu bieżącego połączenia logicznego, zanim zostanie zmieniony.

Od Implicit Async Context ("AsyncLocal")

Oznacza to, że w swojej wersji asyncCallContext.FreeNamedDataSlot("hello") kontynuacją jest zbędny a nawet bez niego:

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 w Main nie będzie zawierać szczelinę "hello":

{miejsce = Task.Run, J = 3, MSG = świecie}
{miejsce = Menem, J = 1, MSG =}

odniesione OC cały kod poza Task.Run (który powinien być Task.Factory.StartNew jako Task.Run został dodany w .Net 4.5) działa na tym samym wątku z dokładnie tak samo CallContext. Jeśli chcesz go czyścić trzeba to zrobić na tym kontekście (a nie w kontynuacji):

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; 
} 

Można nawet Streszczenie W zakres poza nim, aby upewnić się zawsze posprzątać po sobie:

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") 
      })); 
    } 
} 

Zastosowanie:

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

w wersji OC, to istnieje ryzyko, że logiczne CallContext zostanie zwolniona przed Task.Factory.StartNew miał szansę przechwycić? Muszę także mieć pewność, że wszystkie kontynuacje (jeśli istnieją) od Task.Factory.StartNew rzeczywiście mają CallContext, nawet jeśli został "uwolniony" przez główny wątek. –

+0

@BrentArias można przetestować za pomocą Thread.Sleep (ja zrobiłem). Task.Factory.StartNew tak jak Task.Run przechwytuj (kopiuj) kontekst i przechowuj go w zadaniu, aby nie martwić się o to. Więcej informacji na ten temat można znaleźć tutaj: http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx – i3arnon

+0

@BrentArias * "podczas korzystania z Task.Run, call to Run przechwytuje obiekt ExecutionContext z wątku wywołującego, przechowując tę ​​instancję ExecutionContext w obiekcie Task. Gdy delegat przekazany do Task.Run zostanie później wywołany w ramach wykonywania tego zadania, zostanie to wykonane przez ExecutionContext.Run, używając zapisanego kontekstu. Dotyczy to Task.Run, dla ThreadPool.QueueUserWorkItem, dla Delegate.BeginInvoke, dla Stream.BeginRead, dla DispatcherSynchronizationContext.Post i dla każdego innego asynchronicznego API, jakie możesz wymyślić. "* – i3arnon

4

Dobre pytanie. Wersja await może nie działać tak, jak myślisz, że robi to tutaj. Dodajmy kolejną linię rejestrowania wewnątrz 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(); 
    } 
} 

wyjściowa:

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

Zanotuj "world" nadal istnieje po await, ponieważ był tam przed await. I nie ma go po DoSomething().Wait(), ponieważ nie było go wcześniej przed nim.

Co ciekawe, wersja DoSomethingasync tworzy klona kopiowanie przy zapisie z LogicalCallContext do jego zakresu, przy pierwszym LogicalSetData. Czyni to nawet wtedy, gdy nie ma w nim asynchronii - spróbuj await Task.FromResult(0). Zakładam, że cała ExecutionContext zostanie sklonowana do zakresu metody async, po pierwszej operacji zapisu.

OTOH, dla wersji non-asynchronicznym nie ma „logiczny” zakres i żaden zewnętrzny ExecutionContext tutaj, więc klon kopiowanie przy zapisie z ExecutionContext staje się obecny na gwincie Main (ale kontynuacje i Task.Run lambdas jeszcze zdobyć własne klony). Tak, że albo trzeba przenieść CallContext.LogicalSetData("hello", "world") wewnątrz Task.Run lambda lub sklonować kontekst ręcznie:

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; 
}