使用async/await设置Thread.CurrentPrincipal

18
以下是我试图在异步方法中将Thread.CurrentPrincipal设置为一个自定义的UserPrincipal对象的简化版本,但是即使它仍然在新的线程ID 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
}

2
你是否正在使用ASP.NET?如果是,那么你可能想切换到HttpContext.Current.User,它可以给你你所需要的行为。 - Scott Chamberlain
呵呵,我就知道会有这个问题,应该指明要在没有 HttpContext 可用的非 Web 应用程序中工作。 - Sean Merron
1
我认为你不是(在Web应用程序中很少需要使用Task.Run)。 - Scott Chamberlain
如果我将Task.Run逻辑重构为SomeAsyncMethod,然后只需等待SomeAsyncMethod,那么在等待之后,Thread.CurrentPrincipal仍然是WindowsPrincipal而不是UserPrincipal,结果相同。 - Sean Merron
5个回答

15
我知道有线程变化,但认为async/await会为我处理同步问题。
默认情况下,当你使用await等待一个任务时,它会捕获当前的“上下文”(即SynchronizationContext.Current,除非它为空,在这种情况下它是TaskScheduler.Current)。当异步方法恢复时,它将在该上下文中继续执行。
因此,如果您想定义一个“上下文”,可以通过定义自己的SynchronizationContext来实现。这并不是很容易。特别是如果您的应用程序需要在ASP.NET上运行,它需要自己的AspNetSynchronizationContext(它们不能嵌套或任何东西-只有一个)。 ASP.NET使用其SynchronizationContext设置Thread.CurrentPrincipal。
但是,请注意,明确地传递该值通常被认为更合适-无论是显式传递给该方法还是注入到包含此方法的类型的成员变量中。另外,需要注意的是,有一股明显的趋势远离SynchronizationContext。AspNet vNext没有。 OWIN也没有(AFAIK)。自托管SignalR也不支持。

如果你真的不想传递值,那么还有另一种方法:使用ThreadLocalasync等效方式。核心思想是将不可变的值存储在适当继承异步方法的LogicalCallContext中。我在我的博客上介绍了这个“AsyncLocal”(有传言说AsyncLocal可能会在.NET 4.6中出现,但在此之前,您必须自己编写)。请注意,您无法使用AsyncLocal技术读取Thread.CurrentPrincipal;您必须更改所有代码以使用类似于MyAsyncValues.CurrentPrincipal的东西。


2
在4.6中的AsyncLocal不是谣言,它已经存在,没有任何警告表明此功能可能在发布之前被删除。 - Scott Chamberlain
@SeanMerron:对于所有线程本地变量(每个线程都有自己独立的值),这是正确的。如果您的变量是普通静态变量(在所有线程之间共享一个值)或局部变量,则没有问题。 - Stephen Cleary
1
@StephenCleary 的文章关于 AsyncLocal 很棒,但我认为它不适用于他的场景。LogicalCallContext 只会从调用方流向被调用方,如果被调用方添加或更改值,它不会回流到调用方,对吧? - Jeff Cyr
1
@JeffCyr:是的。异步本地变量只能从调用者流向被调用者,因此操作必须在调用之前设置该值。 - Stephen Cleary
1
@Noseratio:说得好;在这种情况下,“AsyncLocal”不会比“CurrentPrincipal”提供任何优势。 - Stephen Cleary
显示剩余3条评论

10

Thread.CurrentPrincipal存储在ExecutionContext中,而ExecutionContext又存储在线程本地存储中。

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

现在你的问题是,在Task.Run内更改CurrentPrincipal,而ExecutionContext只能单向流动。我认为这在大多数情况下是预期的行为,解决方法是在开始时设置CurrentPrincipal。

当在Task内部更改ExecutionContext时,最初想要的结果是不可能实现的,因为Task.ContinueWith也会捕获ExecutionContext。要做到这一点,您必须在委托运行后立即捕获ExecutionContext,然后在自定义awaiter的继续中将其回流,但那将是非常危险的。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sean Merron

8
您可以使用自定义的等待器来传递CurrentPrincipal(或任何线程属性)。下面的示例展示了如何实现,灵感来自Stephen Toub的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);
        }
    }
}

1
我认为这是使它工作的最佳机会,所以我会看看我能做些什么。感谢您抽出时间来组织这个! - Sean Merron

6
ExecutionContext包含SecurityContextCurrentPrincipal,几乎总是在所有异步分支中流动。因此,在您的Task.Run()委托中,您会得到相同的CurrentPrincipal,并且会在一个单独的线程上执行,正如您所注意到的那样。但是,在幕后,您通过ExecutionContext.Run(...)传递上下文,该方法声明:

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

我发现自己处于奇怪的领域,与Stephen Cleary不同 :), 但我不认为SynchronizationContext与此有任何关系。

Stephen Toub在一篇优秀的文章(链接)中涵盖了大部分内容。


我同意。自定义的 SynchronizationContext 无法帮助,因为在 await 继续执行时,CurrentPrincipal 已经被还原了。自定义等待器 可能是一个选择,但任务在等待器的 ICriticalNotifyCompletion.OnCompleted 被调用之前就完成了,这可能会导致竞态条件。 - noseratio - open to work
很棒的文章!感谢sellotape! - Sean Merron

1
我在一款广泛使用的asp.net软件产品上工作,该产品已经存在了10多年...早在async/await引入之前就存在了 - 现在是.net Framework 4.8。其中的一部分使用了类似提问者的设置,我们使用线程来设置主体。
public static void SetContext(int id, string name)
{
    var context = new LoadContext(id);
    var culture = context.Region.CultureInfo;

    Thread.CurrentPrincipal = new PTPrincipal(name, context);
    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
}

这个问题与提问者类似,当使用async await时,上下文设置会丢失。解决方法是在每个awaited调用中使用ConfigureAwait(false),一直到调用堆栈的最后。
private async Task DoSomething()
{
    await Task.Delay(1000).ConfigureAwait(false);
}

.....

await DoSomething().ConfigureAwait(false);

所以,对于我们来说,解决方法就是始终使用.ConfigureAwait(false)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接