AsyncLocal的運作機制和陷阱

来源:http://www.cnblogs.com/zkweb/archive/2017/10/28/7747162.html
-Advertisement-
Play Games

這是今天幫 "檸檬" 分析一個 "AsyncLocal相關的問題" 時發現的. 試想這個代碼輸出的值是多少? 答案是123. 為什麼修改了 的值卻無效呢? 這要從AsyncLocal的運作機制說起. 首先這是 "AsyncLocal的源代碼" : 獲取和設置值用的是 和`ExecutionConte ...


這是今天幫檸檬分析一個AsyncLocal相關的問題時發現的.
試想這個代碼輸出的值是多少?

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

namespace asynclocal
{
    class Program
    {
        public static AsyncLocal<int> v = new AsyncLocal<int>();

        static void Main(string[] args)
        {
            var task = Task.Run(async () =>
            {
                v.Value = 123;
                var intercept = new Intercept();
                await Intercept.Invoke();
                Console.WriteLine(Program.v.Value);
            });
            task.Wait();
        }
    }

    public class Intercept
    {
        public static async Task Invoke()
        {
            Program.v.Value = 888;
        }
    }
}

答案是123.
為什麼修改了AsyncLocal的值卻無效呢?

這要從AsyncLocal的運作機制說起.
首先這是AsyncLocal的源代碼:

public T Value
{
    get
    {
        object obj = ExecutionContext.GetLocalValue(this);
        return (obj == null) ? default(T) : (T)obj;
    }
    set
    {
        ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);
    }
}

獲取和設置值用的是ExecutionContext.GetLocalValueExecutionContext.SetLocalValue這兩個靜態函數.
這兩個靜態函數的源代碼在ExecutionContext中:

internal static object GetLocalValue(IAsyncLocal local)
{
    ExecutionContext current = Thread.CurrentThread.ExecutionContext;
    if (current == null)
        return null;

    object value;
    current.m_localValues.TryGetValue(local, out value);
    return value;
}

internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
{
    ExecutionContext current = Thread.CurrentThread.ExecutionContext ?? ExecutionContext.Default;

    object previousValue;
    bool hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);

    if (previousValue == newValue)
        return;

    IAsyncLocalValueMap newValues = current.m_localValues.Set(local, newValue);

    //
    // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
    //
    IAsyncLocal[] newChangeNotifications = current.m_localChangeNotifications;
    if (needChangeNotifications)
    {
        if (hadPreviousValue)
        {
            Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
        }
        else
        {
            int newNotificationIndex = newChangeNotifications.Length;
            Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
            newChangeNotifications[newNotificationIndex] = local;
        }
    }

    Thread.CurrentThread.ExecutionContext =
        new ExecutionContext(newValues, newChangeNotifications, current.m_isFlowSuppressed);

    if (needChangeNotifications)
    {
        local.OnValueChanged(previousValue, newValue, false);
    }
}

看到SetLocalValue裡面的處理了嗎? 每一次修改值以後都會生成一個新的執行上下文然後覆蓋到當前的線程對象上.

我們再來看看調用一個非同步函數時的代碼:

// Token: 0x06000004 RID: 4 RVA: 0x000020B0 File Offset: 0x000002B0
.method public hidebysig static 
    class [System.Runtime]System.Threading.Tasks.Task Invoke () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
        01 00 21 61 73 79 6e 63 6c 6f 63 61 6c 2e 49 6e
        74 65 72 63 65 70 74 2b 3c 49 6e 76 6f 6b 65 3e
        64 5f 5f 30 00 00
    )
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    // Header Size: 12 bytes
    // Code Size: 52 (0x34) bytes
    // LocalVarSig Token: 0x11000002 RID: 2
    .maxstack 2
    .locals init (
        [0] class asynclocal.Intercept/'<Invoke>d__0',
        [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
    )

    /* 0x000002BC 7309000006   */ IL_0000: newobj    instance void asynclocal.Intercept/'<Invoke>d__0'::.ctor()
    /* 0x000002C1 0A           */ IL_0005: stloc.0
    /* 0x000002C2 06           */ IL_0006: ldloc.0
    /* 0x000002C3 281700000A   */ IL_0007: call      valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
    /* 0x000002C8 7D05000004   */ IL_000C: stfld     valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002CD 06           */ IL_0011: ldloc.0
    /* 0x000002CE 15           */ IL_0012: ldc.i4.m1
    /* 0x000002CF 7D04000004   */ IL_0013: stfld     int32 asynclocal.Intercept/'<Invoke>d__0'::'<>1__state'
    /* 0x000002D4 06           */ IL_0018: ldloc.0
    /* 0x000002D5 7B05000004   */ IL_0019: ldfld     valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002DA 0B           */ IL_001E: stloc.1
    /* 0x000002DB 1201         */ IL_001F: ldloca.s  1
    /* 0x000002DD 1200         */ IL_0021: ldloca.s  0
    /* 0x000002DF 280100002B   */ IL_0023: call      instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class asynclocal.Intercept/'<Invoke>d__0'>(!!0&)
    /* 0x000002E4 06           */ IL_0028: ldloc.0
    /* 0x000002E5 7C05000004   */ IL_0029: ldflda    valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002EA 281900000A   */ IL_002E: call      instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
    /* 0x000002EF 2A           */ IL_0033: ret
} // end of method Intercept::Invoke

非同步函數會編譯成一個狀態機(類型)然後通過System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start執行,
System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start的源代碼如下:

/// <summary>Initiates the builder's execution with the associated state machine.</summary>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
/// <param name="stateMachine">The state machine instance, passed by reference.</param>
[DebuggerStepThrough]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
    if (stateMachine == null) // TStateMachines are generally non-nullable value types, so this check will be elided
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
    }

    // Run the MoveNext method within a copy-on-write ExecutionContext scope.
    // This allows us to undo any ExecutionContext changes made in MoveNext,
    // so that they won't "leak" out of the first await.

    Thread currentThread = Thread.CurrentThread;
    ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher);
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(currentThread, ref ecs);
        stateMachine.MoveNext();
    }
    finally
    {
        ecs.Undo(currentThread);
    }
}

執行狀態機前會調用ExecutionContext.EstablishCopyOnWriteScope, 源代碼如下:

internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw)
{
    Debug.Assert(currentThread == Thread.CurrentThread);

    ecsw.m_ec = currentThread.ExecutionContext;
    ecsw.m_sc = currentThread.SynchronizationContext;
}

執行狀態機後會調用ExecutionContextSwitcher::Undo, 源代碼如下:

internal void Undo(Thread currentThread)
{
    Debug.Assert(currentThread == Thread.CurrentThread);

    // The common case is that these have not changed, so avoid the cost of a write if not needed.
    if (currentThread.SynchronizationContext != m_sc)
    {
        currentThread.SynchronizationContext = m_sc;
    }

    if (currentThread.ExecutionContext != m_ec)
    {
        ExecutionContext.Restore(currentThread, m_ec);
    }
}

總結起來:

  • AsyncLocal每設置一次值就會創建一個新的ExecutionContext並覆蓋到Thread.CurrentThread.ExecutionContext
  • 執行狀態機前會備份當前的Thread.CurrentThread.ExecutionContext
  • 執行狀態機後會恢復備份的Thread.CurrentThread.ExecutionContext

再來看看文章開頭我給出的代碼中的處理流程:

  • 初始的執行上下文為空, 且叫 { }
  • 修改AsyncLocal的值到123後, 執行上下文變為 { <int>: 123 }
  • 調用Intercept.Invoke前備份了執行上下文, 備份的是 { <int>: 123 }
  • Intercept.Invoke修改AsyncLocal的值到888後, 執行上下文變為 { <int>: 888 }
  • 調用Intercept.Invoke後恢復備份的上下文, 恢復後是 { <int>: 123 }

到這裡就很清楚了.
await外的AsyncLocal值可以傳遞到await內, await內的AsyncLocal值無法傳遞到await外(只能讀取不能修改).
這個問題在StackOverflow上有人提過, 但回應很少.

微軟是故意這樣設計的, 否則就無法實現MSDN上的這個例子了.
但我個人認為這是個設計錯誤, 檸檬她給出的例子本意是想在aop攔截器中覆蓋AsyncLocal中的Http上下文, 但明顯這樣做是行不通的.
我建議編寫csharp代碼時儘可能的不要使用ThreadLocal和AsyncLocal.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • WSL,Windows Subsystem for Linux,就是之前的Bash on [Ubuntu on] Windows(嗯,微軟改名部KPI++),在wsl環境下我們可以運行一些Linux程式。 首先要說一句,其實Windows 10在一周年更新(1607,內部版本14393)的時候就加入 ...
  • NTP(Network Time Protocol,網路時間協議)是用來使網路中的各個電腦時間同步的一種協議。它的用途是把電腦的時鐘同步到世界協調時UTC,其精度在區域網內可達0.1ms,在互聯網上絕大多數的地方其精度可以達到1-50ms。 NTP伺服器就是利用NTP協議提供時間同步服務的。 系 ...
  • rpm --import http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-6 執行完成後再按照如圖所示操作 ...
  • 第一步:# vi /etc/sysconfig/network-scripts/ifcfg-bond0 DEVICE=bond0 BONDING_OPTS="mode=0 miimon=100" BOOTPROTO=none ONBOOT=yes BROADCAST=192.168.0.255 IP ...
  • 上篇文章介紹瞭如何在ASP.NET MVC項目中引入Identity組件來實現用戶註冊、登錄及身份驗證功能,並且也提到了Identity是集成到Owin中的,本章就來介紹一下什麼是Owin以及如何使用Owin來增強Identity的功能。 本章的主要內容有: ● 什麼是Owin ● 關於Katana ...
  • 今天跟著學習了一篇關於表格的排序、過濾與分頁功能的博客,下邊分享一下學到的知識與心得: 一、應用之前樣式,增加測試數據 對Views —— Account —— Index.cshtml進行如下修改: (1)應用佈局頁 _LayoutAdmin.cshtml @{ ViewBag.Title = " ...
  • .Net常用類庫 一、String成員方法(常用) 1,bool Contains(string str) 判斷字元串對象是否包含給定的內容 2,bool StartsWith(String str):判斷字元串對象是否以給定的字元串開始。 3,bool EndsWith(String str):判 ...
  • 前言作為一名合格的furry,我不僅要吸娜娜奇,還要天天泡在fa吸大觸們的furry作品,這其中難免遇到某個十分喜愛的作者,於是便想down空此作者的所有作品。鑒於一張張的保存實在費時費力,寫個爬蟲來解決眼前的問題似乎再好不過了,所以便有了現在這個下載器。功能介紹根據作者名批量下載此作者的所有作品,... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...