8

Unten ist eine vereinfachte Version von wo ich Thread.CurrentPrincipal innerhalb einer asynchronen Methode auf ein benutzerdefiniertes UserPrincipal -Objekt festlegen möchte, aber das benutzerdefinierte Objekt verloren geht, nachdem die Wartezeit verlassen, obwohl es noch ist auf der neuen threadID 10.Festlegen von Thread.CurrentPrincipal mit async/await

Gibt es eine Möglichkeit, Thread.CurrentPrincipal innerhalb eines Wartens zu ändern und später zu verwenden, ohne es weiterzugeben oder zurückzugeben? Oder ist das nicht sicher und sollte niemals asynchron sein? Ich weiß, es gibt Thread-Änderungen, aber dachte async/erwarten würde synching dies für mich behandeln.

[TestMethod] 
public async Task AsyncTest() 
{ 
    var principalType = Thread.CurrentPrincipal.GetType().Name; 
    // principalType = WindowsPrincipal 
    // Thread.CurrentThread.ManagedThreadId = 11 

    await Task.Run(() => 
    { 
     // Tried putting await Task.Yield() here but didn't help 

     Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity); 
     principalType = Thread.CurrentPrincipal.GetType().Name; 
     // principalType = UserPrincipal 
     // Thread.CurrentThread.ManagedThreadId = 10 
    }); 
    principalType = Thread.CurrentPrincipal.GetType().Name; 
    // principalType = WindowsPrincipal (WHY??) 
    // Thread.CurrentThread.ManagedThreadId = 10 
} 
+1

Arbeiten Sie mit ASP.NET, wenn so können Sie 'HttpContext.Current.User' wechseln möchten , das gibt dir das Verhalten, nach dem du suchst. –

+0

Hehe, ich dachte, das würde kommen und hätte angeben müssen, dass dies für Nicht-Web-Anwendungen funktioniert, wo HttpContext nicht verfügbar ist. –

+1

Ich habe nicht gedacht, wo Sie sind (Sie wollen sehr selten 'Task.Run' in einer Web-App). –

Antwort

5

Sie könnten einen benutzerdefinierten Warner verwenden, um CurrentPrincipal (oder beliebige Thread-Eigenschaften, für die Angelegenheit) zu fließen. Das folgende Beispiel zeigt, wie es gemacht werden könnte, inspiriert von Stephen Toub's CultureAwaiter. Es verwendet TaskAwaiter intern, sodass auch der Synchronisationskontext (falls vorhanden) erfasst wird.

Verbrauch:

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name); 

await TaskExt.RunAndFlowPrincipal(() => 
{ 
    Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity); 
    Console.WriteLine(Thread.CurrentPrincipal.GetType().Name); 
    return 42; 
}); 

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name); 

-Code (nur sehr wenig getestet):

public static class TaskExt 
{ 
    // flowing Thread.CurrentPrincipal 
    public static FlowingAwaitable<TResult, IPrincipal> RunAndFlowPrincipal<TResult>(
     Func<TResult> func, 
     CancellationToken token = default(CancellationToken)) 
    { 
     return RunAndFlow(
      func, 
      () => Thread.CurrentPrincipal, 
      s => Thread.CurrentPrincipal = s, 
      token); 
    } 

    // flowing anything 
    public static FlowingAwaitable<TResult, TState> RunAndFlow<TResult, TState>(
     Func<TResult> func, 
     Func<TState> saveState, 
     Action<TState> restoreState, 
     CancellationToken token = default(CancellationToken)) 
    { 
     // wrap func with func2 to capture and propagate exceptions 
     Func<Tuple<Func<TResult>, TState>> func2 =() => 
     { 
      Func<TResult> getResult; 
      try 
      { 
       var result = func(); 
       getResult =() => result; 
      } 
      catch (Exception ex) 
      { 
       // capture the exception 
       var edi = ExceptionDispatchInfo.Capture(ex); 
       getResult =() => 
       { 
        // re-throw the captured exception 
        edi.Throw(); 
        // should never be reaching this point, 
        // but without it the compiler whats us to 
        // return a dummy TResult value here 
        throw new AggregateException(edi.SourceException); 
       }; 
      } 
      return new Tuple<Func<TResult>, TState>(getResult, saveState());  
     }; 

     return new FlowingAwaitable<TResult, TState>(
      Task.Run(func2, token), 
      restoreState); 
    } 

    public class FlowingAwaitable<TResult, TState> : 
     ICriticalNotifyCompletion 
    { 
     readonly TaskAwaiter<Tuple<Func<TResult>, TState>> _awaiter; 
     readonly Action<TState> _restoreState; 

     public FlowingAwaitable(
      Task<Tuple<Func<TResult>, TState>> task, 
      Action<TState> restoreState) 
     { 
      _awaiter = task.GetAwaiter(); 
      _restoreState = restoreState; 
     } 

     public FlowingAwaitable<TResult, TState> GetAwaiter() 
     { 
      return this; 
     } 

     public bool IsCompleted 
     { 
      get { return _awaiter.IsCompleted; } 
     } 

     public TResult GetResult() 
     { 
      var result = _awaiter.GetResult(); 
      _restoreState(result.Item2); 
      return result.Item1(); 
     } 

     public void OnCompleted(Action continuation) 
     { 
      _awaiter.OnCompleted(continuation); 
     } 

     public void UnsafeOnCompleted(Action continuation) 
     { 
      _awaiter.UnsafeOnCompleted(continuation); 
     } 
    } 
} 
+1

Ich denke, das ist die beste Chance, diese Arbeit zu machen, also werde ich sehen, was ich damit machen kann. Danke, dass du dir die Zeit genommen hast, das zusammen zu stellen! –

7

Ich weiß, es gibt Änderungen Thread aber dachte async/await würde synching dies für mich behandeln.

async/await führt keine Synchronisierung von faden lokalen Daten von selbst aus. Es hat jedoch einen "Haken", wenn Sie Ihre eigene Synchronisierung durchführen möchten.

standardmäßig, wenn Sie await eine Aufgabe, wird es den curent „Kontext“ erfassen (die SynchronizationContext.Current ist, es sei denn, es null ist, in welchem ​​Fall es TaskScheduler.Current). Wenn die Methode async fortgesetzt wird, wird sie in diesem Kontext fortgesetzt.

Also, wenn Sie einen "Kontext" definieren möchten, können Sie dies tun, indem Sie Ihre eigene SynchronizationContext definieren. Dies ist jedoch nicht gerade einfach. Vor allem, wenn Ihre App auf ASP.NET ausgeführt werden muss, was eine eigene AspNetSynchronizationContext erfordert (und sie können nicht geschachtelt werden oder irgendetwas - Sie erhalten nur einen). ASP.NET verwendet SynchronizationContext, um Thread.CurrentPrincipal festzulegen.

Beachten Sie jedoch, dass es eine eindeutige Bewegung entfernt von SynchronizationContext gibt. ASP.NET vNext hat keinen. OWIN hat es nie getan (AFAIK). Self-Hosted SignalR tut auch nicht. Es wird allgemein als geeigneter erachtet, den Wert irgendeinen Weg zu übergeben - ob dies für die Methode explizit ist oder in eine Elementvariable des Typs, der diese Methode enthält, eingefügt wird.

Wenn Sie wirklich wollen nicht den Wert zu übergeben, dann gibt es einen anderen Ansatz, den Sie auch nehmen kann: ein async -Äquivalent von ThreadLocal. Die Kernidee besteht darin, unveränderliche Werte in einer LogicalCallContext zu speichern, die in geeigneter Weise durch asynchrone Methoden vererbt wird. Ich verdecke diese "AsyncLocal" auf meinem Blog (es gibt Gerüchte von AsyncLocal kommen möglicherweise in .NET 4.6, aber bis dahin müssen Sie Ihre eigenen rollen). Beachten Sie, dass Sie die mit der AsyncLocal Technik nicht lesen können; Sie müssten Ihren gesamten Code ändern, um etwas wie MyAsyncValues.CurrentPrincipal zu verwenden.

+2

'AsyncLocal' in 4.6 ist kein Gerücht, [es ist da] (https://msdn.microsoft.com/en-us/library/dn906268 (v = vs.110) .aspx) ohne Warnung, dass diese Funktion möglicherweise vor der Veröffentlichung entfernt werden. –

+0

Vielen Dank für Ihre schnelle Antwort Stephen! Ich hatte gehofft, dass dies deine Aufmerksamkeit erregen würde :-) Ist das wahr, wenn es um Objekte in einem bestimmten Fall geht oder nur um einen speziellen Fall für Thread.CurrentPrincipal? Zum Beispiel kann ich ein Tier haben, das vom Untertyp Katze ist und es innerhalb einer erwarteten Aufgabe in einen Untertyp Hund umwandeln und es ist immer noch ein Hund nach dem Warten. –

+0

@SeanMerron: Dies gilt für alle Thread-lokalen Variablen (wobei jeder Thread seinen eigenen unabhängigen Wert hat). Wenn Ihre Variable einfach statisch ist (ein Wert, der zwischen allen Threads geteilt wird) oder lokal, dann gibt es kein Problem. –

4

Der Thread.CurrentPrincipal wird im ExecutionContext gespeichert, der im lokalen Threadspeicher gespeichert wird.

Beim Ausführen eines Delegaten in einem anderen Thread (mit Task.Run oder ThreadPool.QueueWorkItem) wird der ExecutionContext aus dem aktuellen Thread erfasst und der Delegat wird in ExecutionContext.Run eingewickelt. Wenn Sie also das CurrentPrincipal vor dem Aufruf von Task.Run setzen, wird es immer noch im Delegate festgelegt.

Jetzt ist Ihr Problem, dass Sie das CurrentPrincipal in Task.Run ändern und der ExecutionContext wird nur in eine Richtung fließen. Ich denke, dies ist das erwartete Verhalten in den meisten Fällen, eine Lösung wäre, das CurrentPrincipal am Anfang zu setzen.

Was ursprünglich gewünscht ist, ist nicht möglich, wenn Sie den ExecutionContext in einer Task ändern, da Task.ContinueWith auch den ExecutionContext erfasst. Um das zu tun, müßten Sie den ExecutionContext direkt nach dem Ausführen des Delegates einfangen und dann in einer Fortsetzung des Custom Waiters weiterleiten, aber das wäre sehr böse.

+0

Danke für die Erklärung des Flusses Jeff. Ich diskutiere auf der benutzerdefinierten Route, aber wie du erwähnt hast, ist es beängstigend. –

2

ExecutionContext, die enthält SecurityContext, die CurrentPrincipal enthält, ist ziemlich viel über alle asynchronen Gabeln geflossen. Also in Ihrem Task.Run() Delegierten, erhalten Sie - in einem separaten Thread, wie Sie bemerken, erhalten Sie CurrentPrincipal. Doch unter der Haube, erhalten Sie das Kontext floss über ExecutionContext.Run(...), in dem es heißt:

Der Ausführungskontext in den vorherigen Zustand zurückgeführt, wenn die Verfahren abgeschlossen ist.

Ich finde mich in seltsames Territorium, das sich mit Stephen Cleary unterscheidet :), aber ich sehe nicht, wie SynchronizationContext irgendetwas damit zu tun hat.

Stephen Toub deckt das meiste davon in einem ausgezeichneten Artikel here ab.

+0

Ich stimme zu. Ein benutzerdefinierter 'SynchronizationContext' würde nicht helfen, da' CurrentPrincipal' zu dem Zeitpunkt wiederhergestellt würde, zu dem 'SynchronizationContext.Post' als Teil einer'Awar'-Fortsetzung aufgerufen wird. Ein [custom waiter] (http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx) könnte eine Option sein, aber es gibt immer eine mögliche Race-Bedingung, dass die Aufgabe vor der Ausführung abgeschlossen wird "ICriticalNotifyCompletion.OnCompleted" von awarter wird aufgerufen. – Noseratio

+0

Toller Artikel! Danke, Tesafilm! –

Verwandte Themen