使用async / await

时间:2015-07-10 17:10:41

标签: c# asynchronous async-await claims-based-identity current-principal

下面是我试图在异步方法中将Thread.CurrentPrincipal设置为自定义UserPrincipal对象的简化版本,但是在离开await后自定义对象会丢失,即使它仍然在新的threadID 10上。

有没有办法在await中更改Thread.CurrentPrincipal并在以后使用它而不传入或返回它?或者这不安全,永远不应该异步?我知道有线程更改,但认为async / await将为我处理同步。

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

4 个答案:

答案 0 :(得分:9)

  

我知道有线程更改,但我认为async / await将为我处理同步。

async / await不会自行同步线程本地数据。它确实有一个"钩子"但是,如果你想进行自己的同步,那就很好了。

默认情况下,当您await任务时,它将捕获当前的"上下文" (SynchronizationContext.Current,除非是null,在这种情况下是TaskScheduler.Current)。当async方法恢复时,它将在该上下文中恢复。

因此,如果您想定义一个"上下文",您可以通过定义自己的SynchronizationContext来实现。但这并不容易。特别是如果您的应用程序需要在ASP.NET上运行,这需要自己的AspNetSynchronizationContext(并且它们不能嵌套或任何东西 - 您只能获得一个)。 ASP.NET使用其SynchronizationContext来设置Thread.CurrentPrincipal

然而,请注意SynchronizationContext有明确的运动。 ASP.NET vNext没有。 OWIN从未做过(AFAIK)。自托管SignalR也不是。通常认为传递值某些方式更合适 - 无论这是对方法是显式的,还是注入到包含此方法的类型的成员变量中。

如果你确实想要传递价值,那么你可以采取另一种方法:async - 相当于{{1 }}。核心思想是将不可变值存储在ThreadLocal中,该值由异步方法适当地继承。我在我的博客上覆盖了这个"AsyncLocal"(有传言说LogicalCallContext可能会出现在.NET 4.6中,但在此之前你必须自己动手)。请注意,您无法使用AsyncLocal技术阅读Thread.CurrentPrincipal;您必须更改所有代码才能使用AsyncLocal

答案 1 :(得分:6)

Thread.CurrentPrincipal存储在ExecutionContext中,该文件存储在线程本地存储中。

在另一个线程上执行委托时(使用Task.Run或ThreadPool.QueueWorkItem),从当前线程捕获ExecutionContext,并将委托包装在ExecutionContext.Run中。因此,如果在调用Task.Run之前设置CurrentPrincipal,它仍将在Delegate中设置。

现在您的问题是您更改了Task.Run中的CurrentPrincipal并且ExecutionContext仅以一种方式流动。我认为这是大多数情况下的预期行为,解决方案是在开始时设置CurrentPrincipal。

在Task中更改ExecutionContext时,您最初想要的是什么,因为Task.ContinueWith也捕获了ExecutionContext。要做到这一点,你必须在委托运行之后立即以某种方式捕获ExecutionContext,然后在自定义awaiter的延续中将其回流,但这将是非常邪恶的。

答案 2 :(得分:6)

您可以使用自定义awaiter来传递CurrentPrincipal(或者任何线程属性)。以下示例显示了如何完成它,受到Stephen Toub's CultureAwaiter的启发。它在内部使用TaskAwaiter,因此也将捕获同步上下文(如果有的话)。

用法:

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

代码(仅经过非常轻微的测试):

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

答案 3 :(得分:4)

ExecutionContext,其中包含SecurityContext,其中包含CurrentPrincipal,几乎总是流经所有异步分叉。因此,在您的Task.Run()委托中,您 - 在您注意到的单独主题中,获得相同的CurrentPrincipal。但是,在幕后,您可以通过ExecutionContext.Run(...)获取上下文,其中包含:

  

当执行上下文返回到先前的状态时   方法完成。

我发现自己处于与Stephen Cleary不同的奇怪领域:),但我不知道SynchronizationContext与此有什么关系。

Stephen Toub在一篇优秀文章here中涵盖了大部分内容。