【问题标题】:How to maintain Thread context across async await model in C#?如何在 C# 中跨异步等待模型维护线程上下文?
【发布时间】:2020-06-18 22:53:41
【问题描述】:

每次等待完成“选项”时是否使用 ThreadStatic 并设置上下文?还有其他方法吗?

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,现在还有其他方法吗?

【问题讨论】:

  • 你真的需要线程ThreadStatic吗?您可以通过CallContext.LogicalSetData/LogicalGetData 流动您的全局状态变量:stackoverflow.com/q/22363830/1768303
  • 或者您可以将其更改为foo.CallAsynx(context);。这就是 ASP.NET MVC 的发展方向。
  • 此外,异步不会创建新线程。我会选择保罗的方法。在 ASP.NET 上下文中,(线程)静态是不安全的,不同的请求在线程池中的线程上运行,因此(线程)静态变量将继续存在并在请求/用户之间共享
  • 我想避免使用 ThreadStatic,这是问这个问题的原因:)。另外,需要考虑的一点是,如果 await 调用抛出异常,我输入的逻辑将不起作用

标签: c# async-await threadstatic


【解决方案1】:

ThreadStaticThreadLocal<T>、线程数据槽和CallContext.GetData/CallContext.SetData 不适用于async,因为它们是特定于线程的。

最好的选择是:

  1. 按照@PauloMorgado 的建议将其作为参数传递。等效地,您可以将其设置为对象的字段成员(通过this 作为参数隐式传递);或者您可以让您的 lambdas 捕获变量(在下面,编译器将通过 this 将其作为参数隐式传递)。
  2. 使用HttpContext.Items(如果您使用的是 ASP.NET 4.5)。
  3. 按照@Noseratio 的建议使用CallContext.LogicalGetData / CallContext.LogicalSetData。您只能将不可变数据存储在逻辑线程上下文中;它仅适用于 .NET 4.5,并非在所有平台上都可用(例如 Win8)。
  4. 通过为该线程安装“主循环”(例如AsyncContext from my AsyncEx library),强制所有async 延续回到同一线程。

【讨论】:

  • 我正在试验 CallContext.LogicalGetData 和 CallContext.LogicalSetData ,看起来这适用于我们的场景。另外,Stephen - 我看到了你关于“隐式异步上下文”的博客。很棒的博客。你知道LogicalGetData/LogicalSetData内部是怎么设计的吗?我希望它不使用 ThreadStatic :)
  • 不,它没有使用ThreadStatic。 :) “逻辑调用上下文”是part of the execution context,当您的代码“流动”到其他线程时,它被框架(浅)复制。在 .NET 4.5 中,逻辑调用上下文具有写入时复制行为,这使其能够按预期为异步代码工作。
  • Stephen, re:"它仅适用于 .NET 4.5,并非在所有平台上都可用(例如 Win8)。"你是说 WinPho 8 吗?
  • @Mike:实际上,我指的是Windows 8 Store Apps。但是,我相信这在 Windows Phone 上也行不通。
  • 这里只想提一下,从.Net 4.6开始就有一个AsyncLocal<T>似乎可以解决这个问题
【解决方案2】:

如果有人在几年后有同样的问题并找到这个线程......

有一个新功能叫做

AsyncLocal<T>

https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=netcore-3.1

这适用于“async/await”,也适用于:

  • Task.Run(...)
  • Dispatcher.BeginInvoke(...)
  • 新线程(...).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

【讨论】:

    猜你喜欢
    • 2013-12-02
    • 2023-01-30
    • 2014-01-23
    • 2015-10-20
    • 1970-01-01
    • 1970-01-01
    • 2020-06-22
    • 2020-07-08
    • 1970-01-01
    相关资源
    最近更新 更多