如何在C#的异步等待模型中保留线程上下文?

10

使用ThreadStatic并在每次await完成后设置上下文“是一种选择吗”? 是否还有其他方法?

public async void Test()
{
    // This is in Thread 1
    Foo foo = new Foo();
    Context.context = "context1"; // This is ThreadStatic
    string result = await foo.CallAsynx();

    // This is most likely Thread 2
    Context.context = "context1";   // This might be a different thread and so resetting context    
}

如果我不想使用ThreadStatic,还有其他的方法吗?


1
你真的需要 ThreadStatic 线程吗? 你可以通过 CallContext.LogicalSetData/LogicalGetData 流传你的全局状态变量。参考链接:https://dev59.com/3H3aa4cB1Zd3GeqPeYio - noseratio - open to work
1
或者你可以将其更改为 foo.CallAsynx(context);。这是 ASP.NET MVC 的做法。 - Paulo Morgado
1
此外,async 不会创建新线程。我会采用 Paulo 的方法。在 ASP.NET 上下文中,(Thread)Static 不安全,不同的请求在来自线程池的线程上运行,因此 (thread)static 变量将存活并在请求/用户之间共享。 - MarkO
我想避免使用ThreadStatic,这就是我提出这个问题的原因。另外需要考虑的一件事是,如果await调用引发异常,那么我放置的逻辑将无法工作。 - Ashwin
2个回答

18

ThreadStatic, ThreadLocal<T>, 线程数据插槽和 CallContext.GetData / CallContext.SetDataasync不兼容,因为它们是特定于线程的。

最好的替代方法是:

  1. 如@PauloMorgado所建议的,将其作为参数传递。同样地,您可以将它设置为对象的字段成员(它将通过this隐式传递为参数);或者您可以让你的lambda捕获变量(在底层,编译器将隐式通过this将其作为参数传递)。
  2. 如果您使用的是ASP.NET 4.5,则可以使用HttpContext.Items。
  3. 如@Noseratio所建议的那样,使用CallContext.LogicalGetData / CallContext.LogicalSetData。您只能在逻辑线程上下文中存储不可变数据;它仅在.NET 4.5上工作,并且不适用于所有平台(例如Win8)。
  4. 通过安装适用于该线程的“主循环”,例如我的AsyncEx库中的AsyncContext,将所有异步继续强制返回到同一个线程。

2
不,它没有使用ThreadStatic。:) “逻辑调用上下文”是执行上下文的一部分,由框架(浅)复制,随着您的代码“流动”到其他线程。在.NET 4.5中,逻辑调用上下文获得了写时复制行为,使其能够按预期工作于异步代码中。 - Stephen Cleary
CallContext.LogicalGetData在运行于Azure的ASP.NET应用程序中是否有效? - Triynko
@Triynko:只要您使用的是.NET 4.5或更高版本,就应该可以。 - Stephen Cleary
11
只是想在这里提一下,自从 .Net 4.6 以来,出现了 AsyncLocal<T> 来解决这个问题。 - AlfeG
@Tamir 我这里有一篇旧博客文章。除此之外,你最好的方法是查看源代码。 :/ - Stephen Cleary
显示剩余4条评论

11

如果有人多年后遇到同样的问题并找到了这个帖子......

现在有一个新功能叫做

AsyncLocal<T>

https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.asynclocal-1?view=netcore-3.1

AsyncLocal可以与 "async/await" 一起使用,同时还可与以下方法一起使用:

  • Task.Run(...)
  • Dispatcher.BeginInvoke(...)
  • new Thread(...).Start()

我刚才使用了以下代码测试这三种情况:

    private void StartTests() {
        Thread.Sleep(1000);
        Task.Run(() => DoWork1());
        Task.Run(() => DoWork2());
    }

    private void DoWork1() {
        ThreadContext.Context.Value = "Work 1";
        Thread.Sleep(5);
        Task.Run(() => PrintContext("1"));
        Thread.Sleep(10);
        Dispatcher.BeginInvoke(new Action(() => PrintContext("1")));
        Thread.Sleep(15);
        var t = new Thread(() => PrintContextT("1"));
        t.Start();
    }

    private void DoWork2() {
        ThreadContext.Context.Value = "Work 2";
        Task.Run(() => PrintContext("2"));
        Thread.Sleep(10);
        Dispatcher.BeginInvoke(new Action(() => PrintContext("2")));
        Thread.Sleep(10);
        var t = new Thread(() => PrintContextT("2"));
        t.Start();
    }

    private void PrintContext(string c) {
        var context = ThreadContext.Context.Value;
        Console.WriteLine("P: " + context + "-" + c);

        Task.Run(() => PrintContext2(c));
    }

    private void PrintContext2(string c) {
        Thread.Sleep(7);
        var context = ThreadContext.Context.Value;
        Console.WriteLine("P2: " + context + "-" + c);
    }

    private void PrintContextT(string c) {
        var context = ThreadContext.Context.Value;
        Console.WriteLine("T: " + context + "-" + c);
    }

    public class ThreadContext {
        public static AsyncLocal<object> Context = new AsyncLocal<object>();
    }

输出:

P: 工作2-2

P: 工作1-1

P2: 工作2-2

P: 工作2-2

P2: 工作1-1

P: 工作1-1

P2: 工作2-2

T: 工作2-2

P2: 工作1-1

T: 工作1-1


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