最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C#.NET AsyncLocal 異步上下文透傳實戰(zhàn)

 更新時間:2026年06月03日 09:18:36   作者:唐青楓  
本文主要介紹了C#.NET AsyncLocal 異步上下文透傳實戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

簡介

異步代碼一多,參數(shù)傳遞很快就會開始變味。

最常見的場景是這樣:

  • 入口層拿到了 TraceId
  • 服務(wù)層要打日志
  • 倉儲層也想拿到同一個 TraceId
  • 調(diào)了好幾層 await 以后,這個值還得一直跟著走

如果每一層都靠方法參數(shù)往下傳,代碼會越來越啰嗦。

這時候很多人會先想到:

  • 放靜態(tài)變量里,不行,會串請求
  • ThreadLocal<T> 里,不穩(wěn),await 之后線程可能早換了

AsyncLocal<T> 就是專門解決這類問題的。

一句話先說透:

AsyncLocal<T> 綁定的不是線程,而是異步調(diào)用鏈的執(zhí)行上下文。

所以這篇文章重點會放在幾件事上:

  • AsyncLocal<T> 到底解決什么問題
  • 為什么它能跨 await 保持值不丟
  • 它和 ThreadLocal<T>、靜態(tài)變量的邊界是什么
  • 哪些場景適合用,哪些場景反而容易踩坑
  • 怎么寫出接近真實項目的 demo,而不是只背幾個 API

AsyncLocal<T>是什么?

AsyncLocal<T> 位于:

System.Threading

它的作用可以直接理解成:

  • 給當(dāng)前異步執(zhí)行流掛一份上下文數(shù)據(jù)
  • 這份數(shù)據(jù)可以穿過 await
  • 后續(xù)方法不用顯式傳參,也能讀到它

先別急著把它想得太玄乎。

它并不是全局變量,也不是線程變量,更不是鎖。

更準(zhǔn)確的說法是:

AsyncLocal<T> 是一份“跟著當(dāng)前邏輯調(diào)用鏈走”的上下文數(shù)據(jù)槽位。

為什么會需要它?

先看一個非常典型的需求。

接口請求進(jìn)來時生成一個鏈路 ID:

string traceId = Guid.NewGuid().ToString("N");

隨后調(diào)用過程可能會經(jīng)過:

  • 控制器
  • 應(yīng)用服務(wù)
  • 領(lǐng)域服務(wù)
  • 倉儲
  • 日志組件

如果每一層都這樣傳:

await service.CreateOrderAsync(orderDto, traceId);

再繼續(xù)往下:

await repository.SaveAsync(order, traceId);

很快就會出現(xiàn)兩個問題:

  • 很多方法參數(shù)只是為了透傳上下文,業(yè)務(wù)本身并不關(guān)心
  • 參數(shù)一多,真正有業(yè)務(wù)意義的輸入反而被淹沒了

這類場景下,AsyncLocal<T> 會比較順手:

  • 請求入口設(shè)置一次
  • 后面整條異步調(diào)用鏈都能讀取
  • 不需要每層顯式帶著跑

它和ThreadLocal<T>的區(qū)別到底在哪?

這個點一定要先分清。

ThreadLocal<T>

綁定的是物理線程。

也就是說:

  • 當(dāng)前線程寫進(jìn)去的值
  • 只能保證當(dāng)前線程繼續(xù)讀到
  • 一旦 await 后續(xù)體切到別的線程,就可能讀不到原來的值

AsyncLocal<T>

綁定的是邏輯執(zhí)行上下文。

也就是說:

  • 即使 await 之后換了線程
  • 只要還在同一條異步調(diào)用鏈上
  • 值通常就還能繼續(xù)拿到

一句話總結(jié)最方便記:

  • ThreadLocal<T> 看線程
  • AsyncLocal<T> 看異步調(diào)用鏈

Demo 1:跨await保持上下文

先看最基礎(chǔ)的例子。

using System;
using System.Threading;
using System.Threading.Tasks;

static AsyncLocal<string> TraceId = new();

static async Task Main()
{
    TraceId.Value = "trace-1001";

    Console.WriteLine($"Main 開始,線程:{Thread.CurrentThread.ManagedThreadId},值:{TraceId.Value}");

    await ProcessAsync();

    Console.WriteLine($"Main 結(jié)束,線程:{Thread.CurrentThread.ManagedThreadId},值:{TraceId.Value}");
}

static async Task ProcessAsync()
{
    Console.WriteLine($"ProcessAsync 開始,線程:{Thread.CurrentThread.ManagedThreadId},值:{TraceId.Value}");

    await Task.Delay(100);

    Console.WriteLine($"ProcessAsync 恢復(fù)后,線程:{Thread.CurrentThread.ManagedThreadId},值:{TraceId.Value}");
}

輸出通常類似這樣:

Main 開始,線程:1,值:trace-1001
ProcessAsync 開始,線程:1,值:trace-1001
ProcessAsync 恢復(fù)后,線程:7,值:trace-1001
Main 結(jié)束,線程:7,值:trace-1001

關(guān)鍵點不是線程號,而是:

  • 線程可能變了
  • TraceId 沒丟

這就是 AsyncLocal<T> 的核心價值。

Demo 2:和ThreadLocal<T>放在一起看,差別會非常直觀

using System;
using System.Threading;
using System.Threading.Tasks;

static ThreadLocal<string> ThreadTrace = new(() => "empty-thread");
static AsyncLocal<string> AsyncTrace = new();

static async Task Main()
{
    ThreadTrace.Value = "thread-trace";
    AsyncTrace.Value = "async-trace";

    await Task.Delay(100).ConfigureAwait(false);

    Console.WriteLine($"ThreadLocal:{ThreadTrace.Value}");
    Console.WriteLine($"AsyncLocal:{AsyncTrace.Value}");
}

這里的結(jié)果最常見的是:

  • ThreadLocal<T> 可能讀到默認(rèn)值,或者讀到當(dāng)前線程自己的那份舊值
  • AsyncLocal<T> 仍然能拿到 async-trace

原因就在于兩者綁定對象根本不同。

AsyncLocal<T>為什么能做到這件事?

背后關(guān)鍵不是 AsyncLocal<T> 自己,而是 .NET 的:

ExecutionContext

可以把它想成一份“當(dāng)前執(zhí)行環(huán)境的上下文包裹”。

這個包裹里可以帶很多信息,其中就包括 AsyncLocal<T> 的值。

當(dāng)異步方法遇到 await 時,運行時通常會做這些事:

  • 先把當(dāng)前執(zhí)行上下文捕獲下來
  • 異步操作完成后,再把這個上下文恢復(fù)回來
  • 于是后續(xù)代碼還能繼續(xù)看到原來的 AsyncLocal<T>

所以真正流動的不是線程,而是上下文。

Demo 3:父流程設(shè)置值,子流程可以直接讀取

using System;
using System.Threading;
using System.Threading.Tasks;

static AsyncLocal<string> CurrentUser = new();

static async Task Main()
{
    CurrentUser.Value = "admin";
    await CreateOrderAsync();
}

static async Task CreateOrderAsync()
{
    Console.WriteLine($"CreateOrderAsync: {CurrentUser.Value}");
    await SaveOrderAsync();
}

static async Task SaveOrderAsync()
{
    await Task.Delay(50);
    Console.WriteLine($"SaveOrderAsync: {CurrentUser.Value}");
}

輸出會保持一致:

CreateOrderAsync: admin
SaveOrderAsync: admin

這正是它在請求上下文、租戶上下文、日志作用域里被頻繁使用的原因。

Demo 4:子流程改值,父流程不會被永久污染

這是 AsyncLocal<T> 很容易讓人誤判的地方。

很多人第一次接觸時,會以為它就是一份所有子流程共享的全局變量。實際上不是。

看例子:

using System;
using System.Threading;
using System.Threading.Tasks;

static AsyncLocal<string> Context = new();

static async Task Main()
{
    Context.Value = "root";

    Console.WriteLine($"Main 調(diào)用前:{Context.Value}");
    await OuterAsync();
    Console.WriteLine($"Main 調(diào)用后:{Context.Value}");
}

static async Task OuterAsync()
{
    Console.WriteLine($"OuterAsync 開始:{Context.Value}");

    Context.Value = "outer";
    await InnerAsync();

    Console.WriteLine($"OuterAsync 結(jié)束:{Context.Value}");
}

static async Task InnerAsync()
{
    Console.WriteLine($"InnerAsync 開始:{Context.Value}");

    Context.Value = "inner";
    await Task.Delay(50);

    Console.WriteLine($"InnerAsync 結(jié)束:{Context.Value}");
}

一類常見輸出會像這樣:

Main 調(diào)用前:root
OuterAsync 開始:root
InnerAsync 開始:outer
InnerAsync 結(jié)束:inner
OuterAsync 結(jié)束:outer
Main 調(diào)用后:root

這個現(xiàn)象最值得記住:

  • 子流程能繼承父流程當(dāng)下的值
  • 子流程里重新賦值,只會影響它自己的那段上下文
  • 父流程恢復(fù)后,還是原來的值

所以更準(zhǔn)確的理解應(yīng)該是:

AsyncLocal<T> 更像“上下文作用域”,不是單純的一份共享變量。

Demo 5:最貼近項目的場景,鏈路追蹤TraceId

這個例子最接近真實項目。

using System;
using System.Threading;
using System.Threading.Tasks;

public static class TraceContext
{
    private static readonly AsyncLocal<string?> _traceId = new();

    public static string? TraceId
    {
        get => _traceId.Value;
        set => _traceId.Value = value;
    }
}

public sealed class OrderService
{
    public async Task CreateAsync()
    {
        Console.WriteLine($"[Service] TraceId={TraceContext.TraceId}");
        await new OrderRepository().SaveAsync();
    }
}

public sealed class OrderRepository
{
    public async Task SaveAsync()
    {
        await Task.Delay(50);
        Console.WriteLine($"[Repository] TraceId={TraceContext.TraceId}");
    }
}

static async Task Main()
{
    TraceContext.TraceId = Guid.NewGuid().ToString("N");

    try
    {
        await new OrderService().CreateAsync();
    }
    finally
    {
        TraceContext.TraceId = null;
    }
}

這種模式非常適合:

  • 鏈路追蹤 ID
  • 當(dāng)前租戶 ID
  • 當(dāng)前請求用戶
  • 日志作用域

最關(guān)鍵的收益是:

  • 業(yè)務(wù)方法簽名不會為了透傳上下文越來越臃腫
  • 底層組件也能直接讀取當(dāng)前上下文

Demo 6:引用類型是個大坑

這點必須單獨講。

很多人知道 AsyncLocal<T> 能隔離上下文,就誤以為里面放引用類型也天然安全。其實并不是。

看這個例子:

using System;
using System.Threading;
using System.Threading.Tasks;

public sealed class RequestInfo
{
    public string TraceId { get; set; } = string.Empty;
}

static AsyncLocal<RequestInfo> RequestContext = new();

static async Task Main()
{
    RequestContext.Value = new RequestInfo { TraceId = "root-trace" };

    await Task.Run(async () =>
    {
        Console.WriteLine($"子任務(wù)開始:{RequestContext.Value.TraceId}");

        RequestContext.Value.TraceId = "child-updated";
        await Task.Delay(50);
    });

    Console.WriteLine($"主流程恢復(fù)后:{RequestContext.Value.TraceId}");
}

這里很可能輸出:

子任務(wù)開始:root-trace
主流程恢復(fù)后:child-updated

原因不是 AsyncLocal<T> 失效了,而是:

  • AsyncLocal<T> 傳的是 RequestInfo 這個引用
  • 子流程和父流程拿到的是同一個對象引用
  • 改的是對象內(nèi)部屬性,不是重新給 .Value 賦新對象

所以實戰(zhàn)里最好優(yōu)先遵循這個原則:

  • 傳簡單值類型
  • 傳字符串
  • 傳不可變對象
  • 盡量少傳可變大對象

如果非要放復(fù)雜對象,最好按“整體替換”來寫,而不是到處改內(nèi)部屬性。

Demo 7:變化通知

AsyncLocal<T> 還有一個不算常用但挺有意思的能力:值變化通知。

using System;
using System.Threading;

var local = new AsyncLocal<string?>(args =>
{
    Console.WriteLine(
        $"上下文變化:舊值={args.PreviousValue ?? "<null>"},新值={args.CurrentValue ?? "<null>"}," +
        $"線程切換={args.ThreadContextChanged}");
});

local.Value = "A";
local.Value = "B";
local.Value = null;

這個能力更適合:

  • 調(diào)試上下文傳播
  • 驗證鏈路是否被改寫
  • 觀察異步恢復(fù)時值變化

業(yè)務(wù)代碼里一般不需要到處用,但排查問題時很有幫助。

它和靜態(tài)變量的邊界是什么?

這一點也很容易混。

靜態(tài)變量

是全局共享的。

如果多個請求同時進(jìn)來,都改同一個靜態(tài)字段,數(shù)據(jù)就會互相覆蓋。

AsyncLocal<T>

通常是“靜態(tài)字段 + 每條異步調(diào)用鏈各自一份值”的組合。

也就是說:

  • 靜態(tài)的是容器入口
  • 真正的值不是全局共享那一份
  • 每條執(zhí)行流看到的是自己的上下文值

這也是為什么框架里很多上下文訪問器,會把 AsyncLocal<T> 寫成 static readonly 字段。

什么時候適合用AsyncLocal<T>?

比較適合:

  • TraceId
  • CorrelationId
  • 當(dāng)前租戶信息
  • 當(dāng)前用戶標(biāo)識
  • 日志上下文
  • 需要跨 await 透傳、但不想層層傳參的控制類信息

不太適合:

  • 大對象緩存
  • 大量頻繁寫入的臨時業(yè)務(wù)數(shù)據(jù)
  • 需要長期?;畹馁Y源對象
  • 復(fù)雜可變對象圖
  • 真正的全局共享狀態(tài)

為什么不建議往里面塞大對象?

原因有兩個。

1. 它的定位不是緩存容器

AsyncLocal<T> 最適合裝的是“小而關(guān)鍵的上下文信息”,比如 ID、名稱、輕量上下文對象。

2. 上下文傳播本身也有成本

異步鏈路越復(fù)雜,傳播越頻繁,塞的對象越重,排查問題和控制生命周期就越麻煩。

尤其是在高并發(fā)服務(wù)里,AsyncLocal<T> 應(yīng)該盡量保持輕量。

后臺任務(wù)場景一定要格外小心

還有一個很容易踩坑的點:

Task.Run 默認(rèn)會捕獲當(dāng)前 ExecutionContext,也就意味著會把當(dāng)前 AsyncLocal<T> 一起帶過去。

這有時是好事,有時反而是壞事。

例如某個請求里啟動了一個后臺任務(wù):

_ = Task.Run(() =>
{
    Console.WriteLine(TraceContext.TraceId);
});

如果本意只是“順手丟個后臺工作”,卻不希望把請求上下文一起傳過去,那這個默認(rèn)行為就可能造成誤判甚至污染。

這種時候要意識到一件事:

AsyncLocal<T> 默認(rèn)是會隨執(zhí)行上下文流動的,不是只在當(dāng)前方法里生效。

高級一點的控制:禁止上下文流動

如果確實明確不想把當(dāng)前上下文傳給后臺任務(wù),可以用:

using System.Threading;

using (ExecutionContext.SuppressFlow())
{
    _ = Task.Run(() =>
    {
        Console.WriteLine(TraceContext.TraceId); // 大概率拿不到原上下文值
    });
}

這個 API 不算日常高頻,但在基礎(chǔ)設(shè)施代碼里很有用。

它適合那種語義非常明確的場景:

  • 就是不希望繼承當(dāng)前請求上下文
  • 就是要啟動一個“脫離當(dāng)前鏈路”的后臺任務(wù)

一個更完整的實戰(zhàn)寫法:帶作用域的上下文包裝

項目里直接到處寫:

TraceContext.TraceId = xxx;

時間長了很容易忘記恢復(fù)。

更穩(wěn)一點的方式是做一個小作用域包裝:

using System;
using System.Threading;

public static class TraceContext
{
    private static readonly AsyncLocal<string?> _traceId = new();

    public static string? TraceId
    {
        get => _traceId.Value;
        private set => _traceId.Value = value;
    }

    public static IDisposable BeginScope(string traceId)
    {
        string? previous = TraceId;
        TraceId = traceId;
        return new RestoreScope(previous);
    }

    private sealed class RestoreScope : IDisposable
    {
        private readonly string? _previous;

        public RestoreScope(string? previous)
        {
            _previous = previous;
        }

        public void Dispose()
        {
            TraceId = _previous;
        }
    }
}

使用時:

using (TraceContext.BeginScope(Guid.NewGuid().ToString("N")))
{
    await service.CreateAsync();
}

這種寫法的好處很直接:

  • 進(jìn)入作用域時設(shè)置值
  • 離開作用域時自動恢復(fù)
  • 比手動清理更不容易漏

它和HttpContext.Items、方法參數(shù)傳遞怎么選?

這幾個工具也經(jīng)常被放在一起比較。

方法參數(shù)傳遞

優(yōu)點是顯式、清楚、最容易追蹤。

缺點是透傳鏈路一長,簽名會越來越臃腫。

HttpContext.Items

適合明確綁定在 ASP.NET Core 請求對象上的數(shù)據(jù)。

但它依賴 HttpContext,脫離 Web 請求上下文就不好用了。

AsyncLocal<T>

適合那種:

  • 需要跨很多層 await
  • 不想層層傳參
  • 又不應(yīng)該做成全局共享變量

最實用的判斷標(biāo)準(zhǔn)可以這么記:

  • 業(yè)務(wù)核心輸入,優(yōu)先參數(shù)傳遞
  • 請求附屬上下文,適合 AsyncLocal<T>
  • 只在 ASP.NET Core 請求里臨時掛點數(shù)據(jù),HttpContext.Items 也可以

總結(jié)

AsyncLocal<T> 最核心的價值,不是“存?zhèn)€值”這么簡單,而是:

  • 讓上下文跟著異步調(diào)用鏈走
  • await 也不容易丟
  • 不用為了透傳上下文把方法簽名寫得越來越重

但邊界也必須記清楚:

  • 它不是線程變量
  • 它不是全局共享變量
  • 它不適合裝大對象和復(fù)雜可變對象
  • 它默認(rèn)會隨著執(zhí)行上下文繼續(xù)傳播

AsyncLocal<T> 不是把數(shù)據(jù)綁在線程上,而是把數(shù)據(jù)掛在當(dāng)前異步調(diào)用鏈上。

到此這篇關(guān)于C#.NET AsyncLocal 異步上下文透傳實戰(zhàn)的文章就介紹到這了,更多相關(guān)C#.NET AsyncLocal 異步上下文透傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C#的回調(diào)機(jī)制淺析

    C#的回調(diào)機(jī)制淺析

    這篇文章主要介紹了C#的回調(diào)機(jī)制,較為通俗的分析了回調(diào)機(jī)制的本質(zhì)與使用時的注意事項,對于C#的學(xué)習(xí)有一定的參考借鑒價值,需要的朋友可以參考下
    2014-11-11
  • C#開發(fā)之微信小程序發(fā)送模板消息功能

    C#開發(fā)之微信小程序發(fā)送模板消息功能

    這篇文章主要介紹了C#開發(fā)之微信小程序發(fā)送模板消息功能,需要的朋友可以參考下
    2017-09-09
  • 把DLL文件打包進(jìn)EXE的操作技巧

    把DLL文件打包進(jìn)EXE的操作技巧

    用VS2005建立一個windows項目,取名test,之后把生成DLL文件打包進(jìn)EXE,接下來介紹詳細(xì)的操作技巧,感興趣的朋友可以了解下啊,或許對你有所幫助
    2013-02-02
  • C#利用WinForm實現(xiàn)查看指定目錄下所有圖片功能

    C#利用WinForm實現(xiàn)查看指定目錄下所有圖片功能

    Windows 窗體是用于生成 Windows 桌面應(yīng)用的 UI 框架, 它提供了一種基于 Visual Studio 中提供的可視化設(shè)計器創(chuàng)建桌面應(yīng)用的高效方法,本文介紹了C#利用WinForm實現(xiàn)可以查看指定目錄文件下所有圖片功能,需要的朋友可以參考下
    2024-05-05
  • C#項目使用obfuscar混淆實踐

    C#項目使用obfuscar混淆實踐

    文章介紹了如何使用Obfuscar混淆C#代碼,包括安裝Obfuscar、配置混淆設(shè)置文件、設(shè)置編譯事件調(diào)用批處理文件以及注意事項,通過這些步驟,每次編譯Release版本時,代碼都會自動混淆,提高安全性
    2025-12-12
  • C#多線程爬蟲抓取免費代理IP的示例代碼

    C#多線程爬蟲抓取免費代理IP的示例代碼

    本篇文章主要介紹了C#多線程爬蟲抓取免費代理IP的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • Unity接入百度AI實現(xiàn)通用物體和場景識別

    Unity接入百度AI實現(xiàn)通用物體和場景識別

    這篇文章主要介紹了在Unity中接入百度AI接口,從而實現(xiàn)通用物體和場景的識別,其中接口返回大類及細(xì)分類的名稱,并支持獲取識別結(jié)果對應(yīng)的百科信息。感興趣的可以學(xué)習(xí)一下
    2022-01-01
  • c# 獲得當(dāng)前絕對路徑的方法(超簡單)

    c# 獲得當(dāng)前絕對路徑的方法(超簡單)

    下面小編就為大家分享一篇c# 獲得當(dāng)前絕對路徑的方法(超簡單),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • C#?比較兩個list?之間元素差異的常用方法

    C#?比較兩個list?之間元素差異的常用方法

    這篇文章主要介紹了C#比較兩個list之間元素差異,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2025-07-07
  • C#實現(xiàn)分治算法求解股票問題

    C#實現(xiàn)分治算法求解股票問題

    本文主要介紹了C#實現(xiàn)分治算法求解股票問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04

最新評論

长阳| 沙洋县| 恭城| 广德县| 天柱县| 泸定县| 洪江市| 建宁县| 廊坊市| 鲜城| 巴马| 周宁县| 团风县| 宽甸| 尉氏县| 吉安市| 乌兰察布市| 贵德县| 宁明县| 兴宁市| 锡林郭勒盟| 清镇市| 容城县| 长丰县| 莒南县| 景东| 会理县| 麻江县| 达孜县| 七台河市| 惠州市| 丹江口市| 基隆市| 汤阴县| 金寨县| 清流县| 新宾| 武功县| 长沙市| 新巴尔虎右旗| 中牟县|