<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          .NET | 多線程下的調(diào)用上下文 : CallContext

          共 9009字,需瀏覽 19分鐘

           ·

          2021-07-05 11:58

          【.NET| 總結(jié)/Edison Zhou

          最近在分析現(xiàn)在團(tuán)隊(duì)的項(xiàng)目代碼(基于.NET Framework 4.5),經(jīng)常發(fā)現(xiàn)一個(gè)CallContext的調(diào)用,記得多年前的時(shí)候用到了它,但是印象已經(jīng)不深刻了,于是現(xiàn)在來復(fù)習(xí)一下。

          1CallContext是個(gè)啥?

          如果說,一個(gè)對(duì)象保證全局唯一,大家肯定會(huì)想到一個(gè)經(jīng)典的設(shè)計(jì)模式:?jiǎn)卫J?。但是,如果要使用的?duì)象必須是線程內(nèi)唯一的呢?

          在.NET Framework中,Microsoft給我們?cè)O(shè)計(jì)了一個(gè)CallContext類。

          • 命名空間:System.Runtime.Remoting.Messaging

          • 類型完全限定名稱:System.Runtime.Remoting.Messaging.CallContext

          CallContext類似于方法調(diào)用的線程本地存儲(chǔ)區(qū)的專用集合對(duì)象,并提供對(duì)每個(gè)邏輯執(zhí)行線程都唯一的數(shù)據(jù)槽。數(shù)據(jù)槽不在其他邏輯線程上的調(diào)用上下文之間共享。當(dāng) CallContext 沿執(zhí)行代碼路徑往返傳播并且由該路徑中的各個(gè)對(duì)象檢查時(shí),可將對(duì)象添加到其中。

          簡(jiǎn)而言之,CallContext提供線程(多線程/單線程)代碼執(zhí)行路徑中數(shù)據(jù)傳遞的能力。

          方法

          描述

          線程安全

          SetData

          存儲(chǔ)給定的對(duì)象并將其與指定名稱關(guān)聯(lián)。

          GetData

          從System.Runtime.Remoting.Messaging.CallContext中檢索具有指定名稱的對(duì)象

          LogicalSetData

          將給定的對(duì)象存儲(chǔ)在邏輯調(diào)用上下文,并將其與指定名稱關(guān)聯(lián)。

          LogicalGetData

           從邏輯調(diào)用上下文中檢索具有指定名稱的對(duì)象。

          FreeNamedDataSlot

          清空具有指定名稱的數(shù)據(jù)槽。

          HostContext

           獲取或設(shè)置與當(dāng)前線程相關(guān)聯(lián)的主機(jī)上下文。在Web環(huán)境下等于System.Web.HttpContext.Current


          2探究CallContext方法

          上面介紹了CallContext提供的核心方法,下面我們就來通過實(shí)踐來理解一下。

          準(zhǔn)備工作

          這里準(zhǔn)備一個(gè)User類作為數(shù)據(jù)傳遞對(duì)象:

          public class User{  public string Id { get; set; }
            public string Name { get; set; }}

          測(cè)試1:GetData、SetData 與 FreeNamedDataSlot

          測(cè)試代碼很簡(jiǎn)單,就是在主線程 和 子線程之中分別傳遞User對(duì)象實(shí)例,看看最后的效果。

          public void TestGetSetData(){    // 主線程執(zhí)行    Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");    var user = new User()    {        Id = DateTime.Now.ToString(),        Name = "Edison"    };    CallContext.SetData("key", user);    var value1 = CallContext.GetData("key");    Console.WriteLine(user == value1);
          // 異步線程執(zhí)行 Task.Run(() => { Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var value2 = CallContext.GetData("key"); Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString()); });
          // 主線程執(zhí)行 Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); value1 = CallContext.GetData("key"); Console.WriteLine(value1 == user);
          // 清理數(shù)據(jù)槽 CallContext.FreeNamedDataSlot("key"); var value3 = CallContext.GetData("key"); Console.WriteLine(value3 == null ? "NULL" : (value3 == value1).ToString());}

          上面示例代碼的運(yùn)行結(jié)果如下圖所示:

          根據(jù)上圖所示的結(jié)果,基本可以得出以下兩個(gè)結(jié)論:

          1、GetData、SetData方法只能用于單線程環(huán)境,如果發(fā)生了線程切換,存儲(chǔ)的數(shù)據(jù)也會(huì)隨之丟失。

          2、GetData 和 SetData 可以用于同一線程中的不同地方,傳遞數(shù)據(jù)

          可以知道,要在多線程環(huán)境下使用,我們需要用到另外兩個(gè)方法:LogicalSetData 與 LogicalGetData。

          測(cè)試2:LogicalGetData、LogicalSetData 與 FreeNamedDataSlot

          public void TestLogicalGetSetData(){    // 主線程執(zhí)行    Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");    var user = new User()    {        Id = DateTime.Now.ToString(),        Name = "Edison"    };    CallContext.LogicalSetData("key", user);    var value1 = CallContext.LogicalGetData("key");    Console.WriteLine(user == value1);
          // 異步線程執(zhí)行 Task.Run(() => { Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var value2 = CallContext.LogicalGetData("key"); Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString());
          Thread.Sleep(1000);
          value2 = CallContext.LogicalGetData("key"); Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString()); });
          // 主線程執(zhí)行 Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); // 清理數(shù)據(jù)槽 CallContext.FreeNamedDataSlot("key"); var value3 = CallContext.LogicalGetData("key"); Console.WriteLine(value3 == null ? "NULL" : (value3 == value1).ToString());}

          這段示例代碼的運(yùn)行結(jié)果如下圖所示:

          據(jù)上圖所示的結(jié)果,基本可以得出以下三個(gè)結(jié)

          1、FreeNamedDataSlot只能清除當(dāng)前線程的數(shù)據(jù)槽,不能清除子線程的數(shù)據(jù)槽;

          2、LogicalSetData、LogicalGetData可用于在多線程環(huán)境下傳遞數(shù)據(jù);

          3、FreeNamedDataSlot清除當(dāng)前線程的數(shù)據(jù)槽后,之前已經(jīng)運(yùn)行的子任務(wù),不受影響

          測(cè)試3:LogicalGetData后修改傳遞的數(shù)據(jù)

          在多線程環(huán)境下傳遞共享對(duì)象數(shù)據(jù),如果某個(gè)線程通過LogicalGetData后對(duì)其進(jìn)行了修改又重新LogicalSetData會(huì)怎樣?

          public void TestLogicalGetSetDataV2(){    // 主線程執(zhí)行    Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");    var user = new User()    {        Id = DateTime.Now.ToString(),        Name = "Edison"    };    CallContext.LogicalSetData("key", user);    var value1 = CallContext.LogicalGetData("key");    Console.WriteLine(user == value1);
          // 異步線程同步執(zhí)行:加了.Wait() Task.Run(() => { Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var value2 = CallContext.LogicalGetData("key"); Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString());
          CallContext.FreeNamedDataSlot("key");
          value2 = CallContext.LogicalGetData("key"); Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString()); }).Wait();
          // 異步線程同步執(zhí)行:加了.Wait() Task.Run(() => { Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var value2 = CallContext.LogicalGetData("key") as User; Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString());
                  value2.Name = "Leo";
          CallContext.LogicalSetData("key", new User() { Id = DateTime.Now.ToString(), Name = "Jack" }); // 只影響當(dāng)前線程 value2 = CallContext.LogicalGetData("key") as User; Console.WriteLine(value2 == null ? "NULL" : (value2 == value1).ToString()); Console.WriteLine($"User.Name={value2.Name}"); }).Wait();
          // 主線程執(zhí)行 Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var value3 = CallContext.LogicalGetData("key") as User; Console.WriteLine(value3 == null ? "NULL" : (value3 == value1).ToString()); Console.WriteLine($"User.Name={value3.Name}");}

          上面示例代碼的運(yùn)行結(jié)果如下圖所示:

          根據(jù)上面的示例運(yùn)行結(jié)果,我們又可以得到以下一些結(jié)論:

          1、FreeNamedDataSlot只能清除當(dāng)前線程的數(shù)據(jù)槽

          2、LogicalSetData只會(huì)存儲(chǔ)當(dāng)前線程以及子線程的數(shù)據(jù)槽

          3、LogicalGetData獲取的是當(dāng)前線程或父線程的數(shù)據(jù)槽對(duì)象,拿到的是對(duì)象的引用,因此如果對(duì)其進(jìn)行修改,會(huì)影響父線程讀取的一致性,在關(guān)系型數(shù)據(jù)庫(kù)中也被稱為不可重復(fù)讀。

          4、子線程中使用LogicalSetData改變數(shù)據(jù)槽的值,不會(huì)影響父線程的數(shù)據(jù)槽,即使他們的key是同一個(gè)

          3.NET Core下沒有CallContext

          在.NET Core下沒有CallContext類,取而代之的是使用AsyncLocal代替,實(shí)現(xiàn)的是CallContext.LogicalGetData 和 CallContext.SetLogicalCallContext。

          例如,下面是一個(gè)示例代碼,我們可以借助AsyncLocal來自己實(shí)現(xiàn)一個(gè)CallContext類。如果你是將.NET Framework升級(jí)為.NET Core,那么你可能需要自己實(shí)現(xiàn)一個(gè)CallContext類來代替之前的CallContext:

          public static class CallContext{    static ConcurrentDictionary<string, AsyncLocal<object>> state = new ConcurrentDictionary<string, AsyncLocal<object>>();
          public static void SetData(string name, object data) => state.GetOrAdd(name, _ => new AsyncLocal<object>()).Value = data;
          public static object GetData(string name) => state.TryGetValue(name, out AsyncLocal<object> data) ? data.Value : null;}
          4EF DbContext場(chǎng)景

          對(duì)于像UnitOfWork這種操作模式,是比較適合于CallContext發(fā)揮的地方,讓EF DbContext在線程上下文內(nèi)保持唯一。

          注意:這里提到的EF均指EF 而非 EF Core。

          因此,我們經(jīng)??梢钥吹饺缦滤镜氖纠a:

          public class DbContextFactory{   public static DbContext CreateDbContext()   {      DbContext dbContext = (DbContext)CallContext.GetData("dbContext");      if (dbContext == null)      {         dbContext = new WebAppEntities();         CallContext.SetData("dbContext", dbContext);      }      return dbContext;   }}

          此用法像極了 Cache(緩存)的使用。

          But,鑒于目前廣泛使用線程池的前提,線程在處理完一個(gè)請(qǐng)求之后,并沒有被銷毀,存儲(chǔ)在CallContext中的上下文對(duì)象也一直存在,如果是下一次拿出這個(gè)線程去處理另一個(gè)請(qǐng)求,這個(gè)上下文對(duì)象其實(shí)也在不斷的膨脹,只不過比全局的膨脹的稍微慢一些。而且,有時(shí)候一個(gè)線程并不一定是拿去處理請(qǐng)求了,如果是服務(wù)器拿去處理其他的業(yè)務(wù),那就可能引發(fā)一些其他的問題。

          這時(shí),或許我們可以考慮另一個(gè)方案,在ASP.NET中的HttpContext中有一個(gè)Items屬性,它也可以用來保存key-value,這就完美了,一次請(qǐng)求正好對(duì)應(yīng)著一個(gè)HttpContext,請(qǐng)求結(jié)束,它自動(dòng)釋放,EF上下文也就不存在了。

          因此,這里把上面代碼中的CallContext改為HttpContext.Current.Items:

          public class DbContextFactory{   public static DbContext CreateDbContext()   {      DbContext dbContext = HttpContext.Current.Items["dbContext"] as DbContext;      if (dbContext == null)      {         dbContext = new WebAppEntities();         HttpContext.Current.Items["dbContext"] = dbContext;      }      return dbContext;   }}

          其實(shí),HttpContext這個(gè)類和CallContext是有關(guān)聯(lián)的,查看源碼我們可以發(fā)現(xiàn):HttpContext.Current是通過CallContext.HostContext實(shí)現(xiàn)的。

          internal static Object Current {   get {     return CallContext.HostContext;   }    [SecurityPermission(SecurityAction.Demand, Unrestricted = true)]   set {     CallContext.HostContext = value;   }}

          關(guān)于HttpContext.Current:ASP.NET會(huì)為每個(gè)請(qǐng)求分配一個(gè)線程,這個(gè)線程會(huì)執(zhí)行我們的代碼來生成響應(yīng)結(jié)果, 即使我們的代碼散落在不同的地方(類庫(kù)),線程仍然會(huì)執(zhí)行它們。所以,我們可以在任何地方訪問HttpContext.Current獲取到與當(dāng)前請(qǐng)求相關(guān)的HttpContext對(duì)象,畢竟這些代碼是由同一個(gè)線程來執(zhí)行的嘛,所以得到的HttpContext引用也就是那個(gè)與請(qǐng)求相關(guān)的對(duì)象。因此,將HttpContext.Current設(shè)計(jì)成與當(dāng)前線程相關(guān)聯(lián)是合適的。有關(guān)CallContext.HostContext的知識(shí)可以自行查閱資料,這里就不再贅述。

          剛剛提到UnitOfWork模式,我們完成了DbContext的線程上下文內(nèi)的唯一性,那么SaveChanges呢?嗯,我們可以基于之前的唯一性保證,來寫一個(gè)SaveChanges的唯一入口。

          public class DbSession{  public static int SaveChanges()  {    return DbContextFactory.GetDbContext().SaveChanges();  }}
          End總結(jié)

          本文簡(jiǎn)單介紹了CallContext類的基本概念、方法,做了一些測(cè)試驗(yàn)證了其提供的方法的適用范圍和限制。

          如果我們需要在.NET代碼中向下傳遞對(duì)象,除了層層遞進(jìn)的傳遞參數(shù)之外,適時(shí)使用CallContext是一個(gè)不錯(cuò)的解耦的方案。


          參考資料

          Microsoft Doc,CallContext

          .NET源碼,https://referencesource.microsoft.com/#System.Web/HttpContext.cs

          雯海,.NET多線程之CallContext(cnblogs博客)

          Koma,EF上下文對(duì)象線程內(nèi)唯一性與優(yōu)化(csdn博客)

          年終總結(jié):Edison的2020年終總結(jié)
          數(shù)字化轉(zhuǎn)型:我在傳統(tǒng)企業(yè)做數(shù)字化轉(zhuǎn)型
          C#刷題:C#刷劍指Offer算法題系列文章目錄
          .NET面試:.NET開發(fā)面試知識(shí)體系
          .NET大會(huì):2020年中國(guó).NET開發(fā)者大會(huì)PDF資料


          歡迎各位讀者加入微信群一起學(xué)習(xí)交流,
          在公眾號(hào)后臺(tái)回復(fù)“加群”即可~~

          ?

          瀏覽 59
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天干天天操天天射 | 日韩中文字幕在线视频 | 久热在线观看视频 | 夏夏粉嫩黑鲍鱼大胆尤物P | 爆操小姐姐 |