2019年6月11日 星期二

.NET Core 的相依性注入的 服務定位器 Service Locator ,如何使用 ServiceProvider 教學

.NET Core 的相依性注入的 服務定位器 Service Locator ,如何使用 ServiceProvider 教學

在 .NET Core 開發框架下,微軟 Microsoft 有提供一個 Microsoft.Extensions.DependencyInjection NuGet 套件,這提供了一個 DI / IoC Container 相依性注入/相依反轉 容器類別庫,雖然這個 DI 容器並沒有提供如同其他 DI 容器的多樣性功能,但是對於要使用相依性注入的開發專案上,已經相當充分與足夠了。
對於相依性注入容器的主要功能,是要能夠提供 3R ,也就是 Register 註冊、解析 Resolve、釋放 Release ,這三大功能。
當建立一個 ASP.NET Core 專案,可以從 Startup.cs 類別中,看到 public void ConfigureServices(IServiceCollection services) 這個方法內,提供了 IServiceCollection 具體實作的物件,開發人員可以透過 IServiceCollection 物件來針對 DI Container 容器來進行抽象型別與具體實作型別(或者只針對具體實作類別) 來進行註冊 Registration。
這篇文章的範例程式碼可以從 GitHub 取得
現在,使用 Visual Studio 2019 建立一個 ASP.NET Coer Web API 類型的專案,接著,打開 Startup.cs 這個檔案,在這裡建立一個新的介面 IMessage 與這個介面的具體實作類別 ConsoleMessage ,如下面的程式碼。
C Sharp / C#
public interface IMessage
{
    string Write(string message);
}
public class ConsoleMessage : IMessage
{
    public string Write(string message)
    {
        string result = $"[Console 輸出] {message}";
        Console.WriteLine(result);
        return result;
    }
}
接下來將需要使用 MS.DI 這個套件,做到服務定位器的功能,也就是說希望能夠透過服務定位器這個物件,進行解析出 IMessage 這個介面所需要用到的具體實作物件。現在需要在 Startup.ConfigureServices 方法內,透過 IServiceCollection services 參數,來進行這個介面與具體實作類別的註冊,也就是要執行 services.AddTransient<IMessage, ConsoleMessage>(); 這個方法,這裡使用了底下的程式碼。
C Sharp / C#
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddTransient<IMessage, ConsoleMessage>();
}
請在 Startup 類別內,找到 Startup.Configure 方法,public void Configure(IApplicationBuilder app, IHostingEnvironment env) ,透過注入的 IApplicationBuilder app 這個物件,裡面有個 ApplicationServices 屬性,這個屬性的型別為 IServiceProvider,在這裡請將這個屬性值指定到新設定的靜態變數 public static IServiceProvider serviceProvider; 內,使用這個敘述 serviceProvider = app.ApplicationServices;
C Sharp / C#
public static IServiceProvider serviceProvider;
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    serviceProvider = app.ApplicationServices;
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
}
為了要能夠使用 DI 容器來進行要注入的型別解析需求,所以,需要取得 IServiceProvider 這個介面的實作物件;不過,要取得這個 IServiceProvider 物件,可以從不同的地方取得,那麼,從不同地方取得的 IServiceProvider 物件,表現上是有什麼不同呢?現在,可以打開 Controllers 資料夾上的 ValuesController.cs 這個檔案,填入底下的程式碼。
在這個 ValuesController 控制器中,將會從底下幾個地方,取得 IServiceProvider:
  • 建構式函式來注入 IServiceProvider serviceProvider
  • 在 Get 方法中,使用 FromServices 屬性,注入 IServiceProvider serviceProvider
  • 在上面的說明內容,有實作出 IApplicationBuilder.ApplicationServices 這個屬性值,並且存放到 Startup.serviceProvider 這個靜態屬性上
  • 最後,透過這個 Action 動作方法中,可以使用 HttpContext.RequestServices 取得 IServiceProvider 這個物件
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
  • 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (34921712),與另外三個管道所取得的 ServiceProvider 的物件 (39157888) 是不同的。
  • 不管是哪個 ServiceProvider 物件,都可以使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件。因為此次註冊的介面與類別,使用的是 AddTransient ,所以,可以看到這裡將會得到不同的 IMessage 實作物件,分別是 : 27973187 , 50833863 , 5822459 , 60684095
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 34921712",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 27973187",
"建構式注入取得的 ServiceProvider 39157888",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 50833863",
"使用 FromServices 屬性取得的 ServiceProvider 39157888",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 5822459",
"從 RequestServices 取得的 ServiceProvider 39157888",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 60684095"
]
C Sharp / C#
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IServiceProvider serviceProvider;

    public ValuesController(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get([FromServices] IServiceProvider actionServiceProvider)
    {
        List<string> result = new List<string>();
        result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
        result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"建構式注入取得的 ServiceProvider {serviceProvider.GetHashCode().ToString()}");
        result.Add($"從 建構式注入取得的 ServiceProvider 解析出 IMessage {serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"使用 FromServices 屬性取得的 ServiceProvider {actionServiceProvider.GetHashCode().ToString()}");
        result.Add($"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage {actionServiceProvider.GetService<IMessage>().GetHashCode().ToString()}");
        result.Add($"從 RequestServices 取得的 ServiceProvider {HttpContext.RequestServices.GetHashCode().ToString()}");
        result.Add($"從 RequestServices 取得的 ServiceProvider 解析出 IMessage {HttpContext.RequestServices.GetService<IMessage>().GetHashCode().ToString()}");
        return result;
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody] string value)
    {
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody] string value)
    {
    }

    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
}
接下來,在 Startup.ConfigureServices 方法內,將 AddTransient 改成 AddSingleton 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
  • 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (48948582),與另外三個管道所取得的 ServiceProvider 的物件 (28785966) 是不同的。
  • 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (53046438)
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 48948582",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 53046438",
"建構式注入取得的 ServiceProvider 28785966",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 53046438",
"使用 FromServices 屬性取得的 ServiceProvider 28785966",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 53046438",
"從 RequestServices 取得的 ServiceProvider 28785966",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 53046438"
]
最後,在 Startup.ConfigureServices 方法內,將 AddSingleton 改成 AddScoped 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,若在同一個 Request 下,所注入的 IMessage 物件,就像是 Singleton 的用法,像是使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
現在,當執行到 result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}"); 敘述之後,竟會得到底下的例外異常訊息:
System.InvalidOperationException: 'Cannot resolve scoped service 'CoreServiceLocator.IMessage' from root provider.'
所以,請把這個 Get 動作內的這兩行敘述先註解起來,然後重新執行一次。
C Sharp / C#
// result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
// result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
  • 現在已經把 IApplicationBuilder.ApplicationServices 的執行程式碼移除了,而透過另外三個管道所取得的 ServiceProvider 的物件 (59274039) 還是都相同的。
  • 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (1177678)
若在同一個 Request 下所做的 Scope 注入行為,就類似於 Singleton 的註冊行為。
[
"建構式注入取得的 ServiceProvider 59274039",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 1177678",
"使用 FromServices 屬性取得的 ServiceProvider 59274039",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 1177678",
"從 RequestServices 取得的 ServiceProvider 59274039",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 1177678"
]



2019年6月4日 星期二

如何在 C# 內,把非同步方法當作同步方法呼叫,卻不會造成執行緒封鎖的做法,徹底解法教學

如何在 C# 內,把非同步方法當作同步方法呼叫,卻不會造成執行緒封鎖的做法,徹底解法教學

現在,在 .NET 開發環境中,原先許多以同步方式處理的 API,現在大多已經提供了非同步的 API,而且這些非同步的 API 並不是使用 APM 或者 EAP 的模式,而是使用 TAP 的工作非同步設計方式。不過,相信絕大部分有在使用 C# 程式語言來進行專案開發的人,都會對於如何充分、正確的使用非同步工作,存在著許多迷惘與困惑,甚至,經常性的會遇到造成執行程式停滯的狀態,無法繼續執行的模式,也就是造成了執行緒封鎖的狀態。若進行 Windows Forms, Web Forms, WPF, ASP.NET MVC 這些類型的專案,因為在其開發框架中有使用到了 SynchronizationContext 同步內容 這個物件,一不小心就會造成程式進入封鎖狀態下。
會造成這樣的模式,大多是把非同步的 API,使用同步的方式來呼叫,例如:有個 async Task<string> MyMethodAsync() 這個非同步方法,並沒有使用 await MyMethodAsync() 這樣的敘述來執行非同步方法,而是使用了 MyMethodAsync().Wait() 或者 MyMethodAsync().Result 的封鎖當前執行緒之同步方式方式來呼叫,這樣的會,當在有 SynchronizationContext 開發框架的模式下,就會造成執行緒封鎖 Thread Block 的狀態。
另外,在某些情況或者是開發框架下所提供的方法內,無法使用 await 來等待一個非同步工作或者非同步方法,例如:當有個方法一定要為 string MyMethod() ,不能夠在前面只加上 async ,也就是寫成 async string MyMethod() 這樣的寫法 (因為,若某個方法前面加上了 async 修飾詞,該方法的回傳值一定要為這三種型別的其中一個: Task , Task , void ),這樣是不正確的,也就是若寫成底下的程式碼,將會造成編譯器產生錯誤訊息 : CS1983 非同步方法的傳回類型必須為 void、Task、Task、task-like 類型、IAsyncEnumerable 或 IAsyncEnumerator SynchronizationContextBlock
C Sharp / C#
async string MyMethod()
{
    return "";
}
若有個方法沒有使用 async 修飾詞,但是會回傳 Task 或者 Task,例如 Task<string> MyTaskAsync()這樣的方法,將會稱為這是一個 非同步工作
若有個方法有使用 async,也就是說,會回傳 Task 或者 Task,例如 async Task<string> MyMethodAsync() 這樣的方法,也就是說,在這個方法內通常會有使用到 await 運算子這個關鍵字,在這裡將會稱為這是一個 非同步方法
底下是微軟文件上對於 async 的說明: 使用 async 修飾詞可將方法、Lambda 運算式或匿名方法指定為非同步。 如果您在方法或運算式上使用這個修飾詞,則它是指「非同步方法」。
await 只能用於 async 關鍵字所修改的非同步方法中。 這種方法是使用 async 修飾詞所定義,且通常包含一或多個 await 運算式,我們稱之為「非同步方法」。
await 運算子會套用至非同步方法中的工作,以在執行方法中插入暫停點,直到等候的工作完成為止。
至於,為什麼會造成這樣的問題,並不是這篇文章要討論的,在這篇文章中,將會說明當某些環境下,一定需要使用同步方式來呼叫各種方法,無法使用 async await 這樣的方式,或者,想不要使用 async await 的方式來呼叫這些非同步 API ;若有這樣的需求,就可以在這篇文章看到各種不同的解決方案,使用同步方式來呼救非同步工作方法,但是卻不會造成執行緒封鎖的問題。

說明要使用的非同步方法程式碼

在這篇文章中的範例,將會使用 WPF 建立起一個專案,並且將會在螢幕上規劃出六個按鈕,每個按鈕皆會有一個 callback 按鈕事件程式碼,當按鈕按下之後,將會要取網路上抓呼叫一個整數相加的 Web API;由於這個呼叫 Web API 的需求將會使用 HttpClient 類別所提供的 GetStringAsync 非同步工作 API,因此,將會設計一個非同步方法,在此方法內使用 await 運算子 ( 當然,在此非同步方法簽章前面要加上 async ),已在執行把這個 HttpClient.GetStringAsync 非同步工作的呼叫中插入暫停點,直到等候該工作完成。
在這裡將會命名這個非同步方法為 MethodAsync,其範例程式碼如下,為了要能夠更清楚的了解到當時執行的執行緒 Thread 為何?因此,會在執行 await 前後加入一個輸出到螢幕上的除錯資訊,這裡將會顯示當前使用的執行緒 ID。這是一個相當單純的非同步方法的設計,也是各位 C# 程式設計師日常會使用到的程式碼。
要這麼設計,是因為要使用 await 來等候一個非同步工作,因此,需要在方法前面加上 async,而又因為回傳值為 string,所以進一步地將 string 回傳型別修改成為 Task<string>
這是一個相當簡單與容易的程式設計方法,可是,接下來就是要探討這麼簡單的方同步方法,在使用上卻會造成專案執行上的災難,也就是造成執行緒打了死結,結果就是該程序無法繼續往下執行下去。
C Sharp / C#
async Task<string> MethodAsync()
{
    Console.WriteLine($"呼叫 MethodAsync 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    //await Task.Delay(1000);
    await new HttpClient().GetStringAsync(url);
    Console.WriteLine($"呼叫 MethodAsync 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    return " @@恭喜你 - MethodAsync 已經執行完畢了@@ ";
}
底下螢幕截圖將會是這個範例程式碼執行的結果,這個範例原始碼可以從 GitHub 取得。

會產生執行緒 Thread 被封鎖 Block 也就是打死結 Deadlock 的用法

在左上角的按鈕,也就是 [直接呼叫非同步方法,會封鎖] 這個按鈕,當按下這個按鈕,將會執行底下程式碼。
這裡並不會在該方法簽章前面加上 async 修飾詞,但是要能夠在這樣的按鈕事件方法內,呼叫一個非同部方法,這個時候,就只能夠使用把非同步方法以同步的方式來呼叫;由於這個非同步方法 MethodAsync() 會回傳 Task<string> 型別,因此,在可以在這個方同步方法上使用 .Result 屬性來取得此非同步方法的回傳結果。
在這裡還是要再度強調,若把非同步方法當作同步方法來呼叫,也就是使用了 Wait() 方法或者使用了 Result 這個屬性,將會造成當前的執行緒進入封鎖 Block 階段,並且會等到非同步方法執行完畢,才會繼續執行下一個 C# 敘述程式碼。
不過,因為這是一個 WPF 的應用程式 (當然, Windows Forms, Web Forms, ASP.NET MVC, Xamarin 類型的專案也會有這樣相同的問題),其該程式在執行的時候,若當前執行的執行緒 Thread 為 主執行緒 或者 UI 執行緒,則該 SynchronizationContext.Current 屬性上是會有值的,而不是 null ;因此,當使用 .Wait() / .Result 這樣的同步方式執行一個非同步方法(注意,這裡指的是非同步方法,不是非同步工作喔,也就是該方法的方法簽章上有加上 async 修飾詞,更技術上的講法,編譯器會針對有 async 的非同步方法,產生許多輔助程式碼,以便完成該方法內的 await 運算子可以插入暫停點以便等候該非同步作業完成),並且當前執行緒為主執行緒,就會造成了主執行緒打死結的情況(各位讀者,你知道為什麼會這樣嗎?歡迎在留言板上寫下你的看法)。
C Sharp / C#
private void BtnWillBlock_Click(object sender, RoutedEventArgs e)
{
    // 請解釋這裡為什麼會產生執行緒封鎖 Block 的狀態
    string result = MethodAsync().Result;
    txtbkMessage.Text = "BtnWillBlock_Click 執行完畢 " + result;
}
所以,當按下這個按鈕,整個程式就凍結了,在 Visual Studio 2019 輸出視窗上,只有看到這個內容輸出。若使用 VS 2019 的除錯工具進行逐步除錯,則會看到當程式執行到 await new HttpClient().GetStringAsync(url); 敘述之後,就再也沒有反映了。
呼叫 MethodAsync 之前,執行緒 ID 1

把非同步方法當作同步方法來使用的解法 1 : 直接呼叫非同步工作,並等候或者取得結果

第一個解法就是如同右上角的按鈕 [直接呼叫非同步工作,為什麼不會封鎖] 的按鈕事件做法,在這裡同樣的在這個按鈕事件中,不要在方法簽章上加上 async 修飾詞,也就是無法在該方法內使用 await 關鍵字,當然,也就使能夠使用同步方法的方式來執行非同步工作或者方法囉。
在此,直接把 非同步方法 MethodAsync() 中用到的 非同步工作 new HttpClient().GetStringAsync(url),直接使用同步方式的呼叫,在此,使用的是 .Wait() 方法。
C Sharp / C#
private void BtnWhyWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    new HttpClient().GetStringAsync(url).Wait();
    txtbkMessage.Text = "BtnWhyWillNotBlock_Click 執行完畢";
}
出乎意外的,這樣的呼叫竟然沒有造成整體程序進入執行緒封鎖狀態,也就是打死結,而是會在螢幕上顯示出 [BtnWhyWillNotBlock_Click 執行完畢,Web API 結果:17] 這樣的內容。
不會造成執行緒封鎖的理由相當的簡單,那就是現在執行的是非同步工作作業,而不是執行非同步非法,因此,不會有 await 這樣插入暫停點與繼續執行的問題。這樣的解釋似乎滿簡單的,不過,理由就是這樣,若想要了解這句話內部的涵義,這可能需要深入到 TPL 以工作為基礎的非同步程式設計 Task-based asynchronous programming 相關知識去了解囉。
總之記得,當前的執行緒有 SynchronizationContext 同步內容,並且有需要要把非同步作業以同步方式來執行,對於非同步工作不會造成問題,然而,對於非同步方法會造成當前執行緒打被封鎖,整體應用程式無法繼續執行。

把非同步方法當作同步方法來使用的解法 2 : 使用 Task.Run 把呼叫非同步方法程式碼包裹起來,並使用 Task.Run 的 Wait() 方法等待作業結束

請點選按鈕 [不會封鎖,使用 Task.Run 包裹起來],可以看到這樣的按鈕事件內的程式碼是不會造成該程序凍結,並且可以從 Visual Studio 2019 輸出 Output 視窗中,看到底下的輸出內容,從這個輸出日誌內容,將會初步了解到整個程序的執行順序。
呼叫 BtnWillNotBlock_Click 之前,執行緒 ID 1
呼叫 await MethodAsync(); 之前,執行緒 ID 3
呼叫 MethodAsync 之前,執行緒 ID 3
呼叫 MethodAsync 之後,執行緒 ID 8
呼叫 await MethodAsync(); 之後,執行緒 ID 8
呼叫 BtnWillNotBlock_Click 之後,執行緒 ID 1
也就是在執行 Task.Run().Result 敘述,使用同步呼叫方式來取得非同步執行結果的前後,所使用的執行緒就是主執行緒,也就是 [執行緒 ID 1]
對於 使用 Task.Run 所傳入的委派方法,這個委派方法是個非同步方法,因為在 Lambda 方法前面有指定 async 修飾詞,因此,可以在此 Lambda 匿名方法內使用 await 運算子關鍵字。但是,在這個委派方法內的所有非同步方法呼叫,是不會造成執行緒封鎖,理由相當的簡單,因為在這個委派方法內,所使用到的執行緒並不是 UI 執行緒,從上面的輸出日誌可以看出,這個時候的執行緒是 執行緒 ID 3 ,不是主執行緒,所以在這裡使用 await 運算子執行非同步方法,是不會有問題的。那麼,能夠理解為什麼這裡委派方法內的程式碼,不是使用到主執行緒嗎?理由也很簡單,那是因為使用了 Task.Run,Task.Run會從執行緒集區 Thread Pool 取得一個執行緒,來執行這個委派方法。
對於執行 Task.Run().Result 敘述也不會造成執行緒封鎖,記得,現在當前的執行緒是 UI執行緒,不過,因為 執行 Task.Run() 是一個非同步工作,不是非同步方法,因此,縱使在主執行下使用同步執行非同步作業,也不會有問題的。
C Sharp / C#
private void btnUsingTaskRunWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    Console.WriteLine($"呼叫 BtnWillNotBlock_Click 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    string result = Task.Run(async () =>
      {
          Console.WriteLine($"呼叫 await MethodAsync(); 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
          string callResult = await MethodAsync();
          Console.WriteLine($"呼叫 await MethodAsync(); 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
          return callResult;
      }).Result;
    Console.WriteLine($"呼叫 BtnWillNotBlock_Click 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    txtbkMessage.Text = "BtnWillBlock_Click 執行完畢 " + result; 
}

把非同步方法當作同步方法來使用的解法 3 : 建立一個新的執行緒,以便可以使用同步方式來呼叫非同步方法

請點選按鈕 [不會封鎖,使用 新的執行緒 包裹起來],可以看到這樣的按鈕事件內的程式碼是不會造成該程序凍結,並且可以從 Visual Studio 2019 輸出 Output 視窗中,看到底下的輸出內容,從這個輸出日誌內容,將會初步了解到整個程序的執行順序。
呼叫 await MethodAsync(); 之前,執行緒 ID 3
呼叫 MethodAsync 之前,執行緒 ID 3
呼叫 MethodAsync 之後,執行緒 ID 10
呼叫 await MethodAsync(); 之後,執行緒 ID 3
0x3784 執行緒以返回碼 0 (0x0) 結束。
呼叫 BtnUsingNewThreadWillNotBlock_Click 之後,執行緒 ID 1
其實,從上一個做法 [使用 Task.Run 把呼叫非同步方法程式碼包裹起來,並使用 Task.Run 的 Wait() 方法等待作業結束] 可以得到一個精神,那就是只要當前的執行緒不是主執行緒或者UI執行緒,這樣來使用 await 來執行非同步方法的話,就不會造成執行緒封鎖。
所以,在這裡將會建立一個執行緒(這是模擬 Task.Run 的作法),在該執行緒的委派方法內來執行非同步方法,不過,這裡有兩點要特別注意的,第一個是,需要使用 Thread.Join() 方法來等待執行緒結束,因為執行緒結束執行了,就可以得到非同步方法的執行結果;第二個是想要取得執行緒內執行結果,需要透過共用變數,在這裡將會使用一個區域變數 result 來做到;最後,因為該執行緒使用的是 new Thread 產生一個執行緒物件,所以,需要手動啟動執行該執行緒,否則,該執行緒是不會執行的。
C Sharp / C#
private void BtnUsingNewThreadWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    string result = "";
    Thread thread = new Thread(async x =>
    {
        Console.WriteLine($"呼叫 await MethodAsync(); 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
        result = MethodAsync().Result;
        Console.WriteLine($"呼叫 await MethodAsync(); 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    })
    { IsBackground = true };
    thread.Start();
    thread.Join();
    Console.WriteLine($"呼叫 BtnUsingNewThreadWillNotBlock_Click 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    txtbkMessage.Text = "BtnUsingNewThreadWillNotBlock_Click 執行完畢 " + result;
}

把非同步方法當作同步方法來使用的解法 4 : 自行重新設定 SynchronizationContext,以便可以使用同步方式來呼叫非同步方法

請點選按鈕 [不會封鎖,使用重新設定 ResetSynchronizationContext],可以看到這樣的按鈕事件內的程式碼是不會造成該程序凍結,並且可以從 Visual Studio 2019 輸出 Output 視窗中,看到底下的輸出內容,從這個輸出日誌內容,將會初步了解到整個程序的執行順序。
呼叫 BtnResetSynchronizationContextWillNotBlock_Click 之前,執行緒 ID 1
呼叫 MethodAsync 之前,執行緒 ID 1
呼叫 MethodAsync 之後,執行緒 ID 7
呼叫 BtnResetSynchronizationContextWillNotBlock_Click 之後,執行緒 ID 1
前面的兩種解法個人認為有一個滿嚴重的問題,那就每次要使用同步方式來呼叫非同步方法的時候,都需要花費一個額外的執行緒來避免主執行緒不會造成封鎖的狀態,對於某些開發框架之下,例如:ASP.NET MVC 的專案下,過度消耗執行緒,並不是一個很好的做法,那會造成整體網站的可用執行緒減少,因此,需要找出另外一個比較好的解法。
現在,可以使用底下的作法,雖然會增加一些額外的程式碼,不過卻不需要額外花費一個執行緒來解決執行緒封鎖的問題;當想要使用同步方式來呼叫非同步方法的時候,先使用 SynchronizationContext.Current 屬性取得當前執行緒(主執行緒或者 Request 執行緒)的 SynchronizationContext,將其儲存起來,接著將 SynchronizationContext 物件儲存起來,接著使用 SynchronizationContext.SetSynchronizationContext(null) 將 SynchronizationContext 設定為空值,也就是不存在,此時,就可以放心的使用同步方式來呼叫非同步方法或者非同步工作了,這樣的作法是不會造成執行緒封鎖的。在完成以同步方式呼叫非同步方法的時候,就可以使用 SynchronizationContext.SetSynchronizationContext(synchronizationContext) 將剛剛儲存起來的 SynchronizationContext 物件,設定到當前執行緒上。
C Sharp / C#
private void BtnResetSynchronizationContextWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    Console.WriteLine($"呼叫 BtnResetSynchronizationContextWillNotBlock_Click 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    SynchronizationContext synchronizationContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);
    string result = MethodAsync().Result;
    SynchronizationContext.SetSynchronizationContext(synchronizationContext);
    Console.WriteLine($"呼叫 BtnResetSynchronizationContextWillNotBlock_Click 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    txtbkMessage.Text = "BtnResetSynchronizationContextWillNotBlock_Click 執行完畢 " + result;
}

把非同步方法當作同步方法來使用的解法 5 : 設計一個泛型類別,以便可以使用同步方式來呼叫非同步方法

請點選按鈕 [不會封鎖,使用 Task Library],可以看到這樣的按鈕事件內的程式碼是不會造成該程序凍結,並且可以從 Visual Studio 2019 輸出 Output 視窗中,看到底下的輸出內容,從這個輸出日誌內容,將會初步了解到整個程序的執行順序。
呼叫 BtnUsingTaskLibraryWillNotBlock_Click 之前,執行緒 ID 1
呼叫 MethodAsync 之前,執行緒 ID 3
呼叫 MethodAsync 之後,執行緒 ID 8
呼叫 BtnUsingTaskLibraryWillNotBlock_Click 之後,執行緒 ID 1
這裡的作法與使用 Task.Run 的做法有些類似,其實都是使用額外新的執行緒來做到可以使用同步方式來呼叫非同步方法,現在將會建立一個泛型方法 InvokeAsyncMethod,在這裡會使用 Task.Factory.StartNew 來建立一個新的非同步工作,把傳入進來的非同步委派方法,使用 .Unwrap().GetAwaiter().GetResult() 來等待非同步方法的結束執行。
使用的方式可以像是這個按鈕事件中的用法 : InvokeAsyncMethod(() => MethodAsync()) 直接把非同步方法轉換成為同步方式呼叫,但是沒有使用到任何 Wait() 方法或者 Result 屬性。
C Sharp / C#
private void BtnUsingTaskLibraryWillNotBlock_Click(object sender, RoutedEventArgs e)
{
    Console.WriteLine($"呼叫 BtnUsingTaskLibraryWillNotBlock_Click 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    string result = InvokeAsyncMethod(() => MethodAsync());
    Console.WriteLine($"呼叫 BtnUsingTaskLibraryWillNotBlock_Click 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
    txtbkMessage.Text = "BtnUsingTaskLibraryWillNotBlock_Click 執行完畢 " + result;
}

T InvokeAsyncMethod<T>(Func<Task<T>> func)
{
    return Task.Factory.StartNew(func)
        .Unwrap()
        .GetAwaiter()
        .GetResult();
}





2019年6月3日 星期一

如何在 Visual Studio 2019 中針對 .NET Core 原始檔進行偵錯

如何在 Visual Studio 2019 中針對 .NET Core 原始檔進行偵錯

有些時候,想要知道 .NET BCL 中的程式碼究竟做了甚麼事情,此時,可以依照這篇文章的教學步驟說明,先進行 Visual Studio 2019 的相關設定,以便建立的專案可以針對 Base Class Library 的 API 進行偵錯;在這裡將會建立一個 .NET Core 2.2 版本的主控台應用程式專案來展示相關過程。
首先,打開 Visual Studo 2019
從 Visual Studio 2019 功能表來點選 [工具] > [選項]
在選項對話窗開啟之後,點選 [偵錯] > [一般]
請勾選 [啟用 .NET Framework 原始碼逐步偵錯]
此時,會有一個警告對話窗出現,啟用 .NET Framework 原始碼逐步偵錯時會自動停用 Just My Code ,請直接點選 [確定] 按鈕
在選項對話窗開啟之後,點選 [偵錯] > [符號]
請勾選 [符號檔 (.pdb) 位置] 標籤下方的 [Microsoft 符號伺服器] 選項,並且在下方的 [快取此目錄中的符號] 標籤下方的文字輸入盒,輸入一個目錄將要用來儲存從網路下載的 符號檔 Symbol File .pdb 檔案。
最後,點選 [確定] 按鈕
現在,可以建立一個 .NET Core 2.2 的主控台應用程式,並且填入底下程式碼。
在 list.Add("123"); 行敘述上設定一個中斷點
開始執行這個專案,此時,因為是第一次執行 .NET 原始碼逐步偵錯,所以,會下載相關符號檔,所以,需要花些時間,之後程式就會停在剛剛設定的中斷點上。
按下 [F11] 或者點選 [逐步執行] 工具列按鈕,就會到底下的內容,現在 Visual Studio 切換 List.cs 這個視窗中的 Add 方法上,這些程式碼就是 .NET BCL 上的原始碼。
而在剛剛設定的 下載的 符號檔 Symbol File .pdb 檔案 目錄中,將會看到成功下載的符號檔案
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        List<string> list = new List<string>();
        list.Add("123");
        Console.WriteLine("Hello World!");


        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
}