針對 .NET / CLR / C# / Blazor / MAUI / Xamarin / .NET Core / .NET Framework / OOP / Design Pattern 等相關程式開發議題進行研究與寫成相關心得筆記。
2017年10月16日 星期一
2017年10月15日 星期日
C# 非同步程式設計的 async void 與 async Task 的差異
若你正在觀看此篇文章,那麼你將會對於 為什麼需要使用非同步程式設計,真的可以提升整體應用程式的執行效能嗎? 問題更有興趣的。
今天,想來談談絕大部分的 C# 開發者都會產生這樣的不正確的 C# 程式碼寫法與用法,那就是當我們採用 工作式非同步模式 (TAP, Task-based Asynchronous Pattern) 進行非同步的程式開發的時候,並且想要寫個非同步運作的分法,在很多時候,您會想說,底下的方法,就是一個非同步運作的程式碼方法。
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
在這裡,我們在方法 M1 前面使用了 async 修飾詞,不過,這個方法並沒有任何物件值需要回傳,因此,在底下的程式碼中,您看到了這個方法使用了
async void
。
在工作式非同步模式的方法中,回傳值的類型僅有 Task / Task<T>,前者代表沒有任何實際物件要回傳,而後者表示要回傳型別為 T 的物件;不過,那我們為什麼又可以使用 void 這個關鍵字呢?通常,我們會在綁定事件的方法上,需要使用
async void
這樣的標示,這是因為這些事件的函式簽章就是僅支援 void 這樣的回傳值,另外,我們在這個綁定的事件方法中,又需要撰寫工作式非同步模式的功能,因此,又需要在這個事件方法前面,加入 async
這個修飾詞。所以,除了是在綁定事件的方法內,其他任何情況下,對於您開發的工作式非同步模式方法,就不需要使用
async void這樣的回傳值標示。
上面的說明,很重要,很重要,很重要
public async void M1()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 開始時間 : {foo}");
await Task.Delay(3000);
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 結束時間 : {foo}");
}
現在,讓我們來看看這個方法實際使用的情況,首先,我們定義一個類別 A,它的定義如下程式碼,其中, M1 是我們使用
async void
的方式建立的方法,而 M2,則是正常的工作式非同步模式方法的程式碼寫法。class A
{
public async void M1()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 開始時間 : {foo}");
await Task.Delay(3000);
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 結束時間 : {foo}");
}
public async Task M2()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 開始時間 : {foo}");
await Task.Delay(3000);
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 結束時間 : {foo}");
}
}
M1這個方法,會先顯示這個方法的開始執行時間,接著,便會暫停3秒鐘,接著便會顯示結束執行這個方法的時間。
在測試程式碼中,我們先建立一個類別A的物件 objA,接著呼叫 objA.M1() 方法,最後,等候使用按下任一按鍵。
class Program
{
static async Task Main(string[] args)
{
A objA = new A();
objA.M1();
Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
Console.ReadKey();
}
}
上面的程式碼執行結果如底下所示,你可以看到,當呼叫 M1 方法之後,接著, M1 方法似乎就以另外一個執行緒的方式進行執行,所以, Main 這個方法內的程式碼,沒有等到 M1 執行完畢,就直接執行
objA.M1()
之後的所有敘述,而當三秒鐘之後,就會顯示 M1 方法已經結束的時間。
這是因為我們使用了
async void
的方式來宣告這個方法,因此,我們沒有任何方法,可以等候 M1 方法執行完成之後,才要繼續執行 objA.M1()
之後的所有敘述。M1 開始時間 : 09:23.1819
Press any key to Exist...
M1 結束時間 : 09:26.2171
現在,讓我們修改測試程式碼,我們在呼叫 M1 方法前後,與在 M1 方法內,當要呼叫
await Task.Delay(3000);
的前後,都顯示出當時執行緒的 ID (這裡顯示的是受管理的執行緒ID) Thread.CurrentThread.ManagedThreadId
。class A
{
public async void M1()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 開始時間 : {foo}");
Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(3000);
Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 結束時間 : {foo}");
}
public async Task M2()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 開始時間 : {foo}");
await Task.Delay(3000);
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 結束時間 : {foo}");
}
}
class Program
{
static async Task Main(string[] args)
{
A objA = new A();
Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
objA.M1();
Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
Console.ReadKey();
}
}
此時,執行結果將會如下所示,您會看到,當 M1 的方法內,其
await Task.Delay(3000);
敘述執行完畢之後,此時的執行緒 ID 變成了 4。Main 1
M1 開始時間 : 20:55.2604
M1 1
Main 1
Press any key to Exist...
M1 4
M1 結束時間 : 20:58.2897
最後,讓我們加入 M2 的方法呼叫,對於要呼叫 M2 的非同步方法,我們使用了
await
關鍵字 await objA.M2();
。class A
{
public async void M1()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 開始時間 : {foo}");
Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(3000);
Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M1 結束時間 : {foo}");
}
public async Task M2()
{
var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 開始時間 : {foo}");
await Task.Delay(1000);
foo = DateTime.UtcNow.ToString("mm:ss.ffff");
Console.WriteLine($"M2 結束時間 : {foo}");
}
}
class Program
{
static async Task Main(string[] args)
{
A objA = new A();
Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
objA.M1();
await objA.M2();
Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
Console.ReadKey();
}
}
底下將為這個測試程式碼的執行輸出結果,我們可以看到, objA.M1() 方法一執行到
await Task.Delay(3000);
敘述之後,就立即返回到 Main 方法內,接著執行 await objA.M2();
,不過,此時,整個程式將會等候到 objA.M2() 方法執行完後,才會繼續進行下去,而在這個時候, M1 的方法,也持續在等候 Task.Delay 的甦醒時間ain 1
M1 開始時間 : 24:53.7485
M1 1
M2 開始時間 : 24:53.7645
M2 結束時間 : 24:54.7670
Main 4
Press any key to Exist...
M1 5
M1 結束時間 : 24:56.7678
訂閱:
文章 (Atom)
HttpClient 系列文章索引
後端 Web API 的原始碼
namespace VulcanWebAPI.Controllers { [Route("api/[controller]")] public class ValuesController : Controller { // http://vulcanwebapi.azurewebsites.net/api/values [HttpGet] public APIResult Get() { APIResult foo = new APIResult() { Success = true, Message = "成功得所有資料集合", Payload = new List<APIData>() { new APIData() { Id =777, Name = "Vulcan01" }, new APIData() { Id =234, Name ="Vulcan02" } } }; return foo; } [HttpGet("QueryStringGet")] public APIResult QueryStringGet([FromQuery] APIData value) { APIResult foo; if (value.Id == 777) { foo = new APIResult() { Success = true, Message = "透過 Get 方法,接收到 Id=777", Payload = new APIData() { Id = 777, Name = "Vulcan by QueryStringGet" } }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } [HttpGet("GetException")] public APIResult GetException() { APIResult foo = new APIResult(); throw new Exception("喔喔,我發生錯誤了"); return foo; } [HttpGet("GetExceptionFilter")] [CustomExceptionFilter] public APIResult GetExceptionFilter() { APIResult foo = new APIResult(); throw new Exception("喔喔,我發生錯誤了"); return foo; } // http://vulcanwebapi.azurewebsites.net/api/values/777 [HttpGet("{id}")] public APIResult Get(int id) { APIResult foo; if (id == 777) { foo = new APIResult() { Success = true, Message = "透過 Get 方法,接收到 Id=777", Payload = new APIData() { Id = 777, Name = "Vulcan01" } }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } // http://vulcanwebapi.azurewebsites.net/api/Values // 使用 JSON 格式 [HttpPost] public APIResult Post([FromBody]APIData value) { APIResult foo; if (value.Id == 777) { foo = new APIResult() { Success = true, Message = "透過 post 方法,接收到 Id=777 資料", Payload = value }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } // http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost // 使用 FormUrlEncodedContent [HttpPost("FormUrlencodedPost")] public APIResult FormUrlencodedPost([FromForm]APIData value) { APIResult foo; if (value.Id == 777) { foo = new APIResult() { Success = true, Message = "透過 post 方法,接收到 Id=777 資料", Payload = value }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } [HttpPut] public APIResult Put(int id, [FromBody]APIData value) { APIResult foo; if (value.Id == 777) { foo = new APIResult() { Success = true, Message = "透過 Put 方法,接收到 Id=777 資料", Payload = value }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } [HttpDelete("{id}")] public APIResult Delete(int id) { APIResult foo; if (id == 777) { foo = new APIResult() { Success = true, Message = "Id=777 資料 已經刪除了", Payload = null }; } else { foo = new APIResult() { Success = false, Message = "無法發現到指定的 ID", Payload = null }; } return foo; } // http://vulcanwebapi.azurewebsites.net/api/values/HeaderPost [HttpPost("HeaderPost")] public APIResult HeaderGet([FromBody]LoginInformation loginInformation) { APIResult foo; StringValues VerifyCode = ""; this.HttpContext.Request.Headers.TryGetValue("VerifyCode", out VerifyCode); if (StringValues.IsNullOrEmpty(VerifyCode)) { foo = new APIResult() { Success = false, Message = "驗證碼沒有發現", Payload = null }; } else { if (VerifyCode != "123") { foo = new APIResult() { Success = false, Message = "驗證碼不正確", Payload = null }; } else { if (loginInformation.Account == "Vulcan" && loginInformation.Password == "123") { foo = new APIResult() { Success = true, Message = "這個帳號與密碼正確無誤", Payload = null }; } else { foo = new APIResult() { Success = false, Message = "這個帳號與密碼不正確", Payload = null }; } } } return foo; } [HttpPost("Login")] public async Task<APIResult> Login([FromBody]LoginInformation loginInformation) { APIResult foo; if (loginInformation.Account == "Vulcan" && loginInformation.Password == "123") { var claims = new List<Claim>() { new Claim(ClaimTypes.Name, "Herry"), new Claim(ClaimTypes.Role, "Users") }; var claimsIdentity = new ClaimsIdentity(claims, "myTest"); var principal = new ClaimsPrincipal(claimsIdentity); try { await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(20), IsPersistent = true, AllowRefresh = true }); foo = new APIResult() { Success = true, Message = "這個帳號與密碼正確無誤", Payload = null }; } catch (Exception ex) { Console.WriteLine(ex.Message); foo = new APIResult() { Success = false, Message = "這個帳號與密碼不正確", Payload = null }; } } else { foo = new APIResult() { Success = false, Message = "這個帳號與密碼不正確", Payload = null }; } return foo; } [Authorize] [HttpGet("LoginCheck")] public APIResult LoginCheck() { APIResult foo = new APIResult() { Success = true, Message = "成功得所有資料集合", Payload = new List<APIData>() { new APIData() { Id =777, Name = "Vulcan01" }, new APIData() { Id =234, Name ="Vulcan02" } } }; return foo; } [HttpGet("LongTimeGet")] public async Task<APIResult> LongTimeGet() { APIResult foo; await Task.Delay(5000); foo = new APIResult() { Success = true, Message = "透過 Get 方法", Payload = new APIData() { Id = 777, Name = "Vulcan01" } }; return foo; } } }
namespace VulcanWebAPI.Controllers { [Route("api/[controller]")] public class UploadController : Controller { APIResult fooAPIResult = new APIResult(); private IHostingEnvironment _HostingEnvironment; public UploadController(IHostingEnvironment hostingEnvironment) { _HostingEnvironment = hostingEnvironment; } [HttpPost] public async Task<APIResult> Post(List<IFormFile> files) { // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas"); long size = files.Sum(f => f.Length); // full path to file in temp location if (files.Count > 0) { foreach (var formFile in files) { if (formFile.Length > 0) { var filePath = Path.Combine(webDatasRoot, formFile.FileName); using (var stream = new FileStream(filePath, FileMode.Create)) { await formFile.CopyToAsync(stream); } fooAPIResult.Success = true; fooAPIResult.Message = "檔案上傳成功"; fooAPIResult.Payload = new APIData { Id = 3000, Name = "Your Name", Filename = formFile.FileName }; } } } else { fooAPIResult.Success = false; fooAPIResult.Message = "沒有任何檔案上傳"; fooAPIResult.Payload = null; } return fooAPIResult; } [HttpPost("FileAndData")] public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation) { // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas"); long size = files.Sum(f => f.Length); // full path to file in temp location if (files.Count > 0) { foreach (var formFile in files) { if (formFile.Length > 0) { using (var memoryStream = new MemoryStream()) { await formFile.CopyToAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(memoryStream); var fooContent = streamReader.ReadToEnd(); fooAPIResult.Success = true; fooAPIResult.Message = "檔案上傳成功"; fooAPIResult.Payload = new LoginInformation { Account = $">> {loginInformation.Account}", Password = $">> {loginInformation.Account}", VerifyCode = fooContent }; } } } } else { fooAPIResult.Success = false; fooAPIResult.Message = "沒有任何檔案上傳"; fooAPIResult.Payload = null; } return fooAPIResult; } } }
關於 Xamarin 在台灣的學習技術資源