作者:Ron.Liang
連結:https://www.cnblogs.com/viter/p/10271212.html
目錄
-
前言
-
1、異常的發生來得太突然
-
2、問題所在
-
3、問題的解決方案
前言
事情的起因是由於一段簡單的資料庫連線程式碼引起,這段程式碼從語法上看,是沒有任何問題;但是就是莫名其妙的報錯了,這段程式碼極其簡單,就是開啟資料庫連線,讀取一條記錄,然後立即更新到資料庫中。但是,慘痛的事實證明,老司機也是會翻車的。
1、異常的發生來得太突然
1.1 引起不舒適的程式碼片段
[
]
public async void Put([FromBody] TopicViewModel model)
{
var topic = this.context.Topics.Where(f => f.Id == model.Id).FirstOrDefault();
topic.Content = model.Content;
this.context.Update(topic);
var affrows = await this.context.SaveChangesAsync();
}
這是一段不太標準的非同步介面,可能你也這麼寫過, 從結構和語法上看,這段程式碼沒有任何問題,而且正常情況下,它還能執行成功
1.2 報錯資訊
從報錯資訊中可以看出,資料庫背景關係物件被銷毀了,是在什麼時候銷毀的呢,透過跟蹤程式,瞭解到,是在 this.context.Update(topic); ,呼叫 Update 後執行了 DbContext.Dispose(),為了證明這點,我們重寫 DbContext.Dispose() 方法,並簡單的輸出一句話
1.3 重寫 DbContext.Dispose()
public class ForumContext : DbContext
{
public ForumContext(DbContextOptionsoptions ) : base(options)
{
}
public DbSetTopics { get; set; }
public DbSetPosts { get; set; }
public override void Dispose()
{
base.Dispose();
Console.WriteLine("Dispose");
}
}
1.4 再次執行程式,檢視結果
透過輸出結果紅色方框處可以看到,確實是在執行了 Update 以後執行了 Dispose 方法,關於這點,如果我們使用了同步方法,先 Update 再 SaveChanges ,這是沒有任何問題的,理論上說,EFCore 中啟用了 AutoDetectChangesEnabled,我們在上面的程式碼中其實無需呼叫 Update,直接 SaveChangesAsync 即可,也不會丟擲異常,同理,如果是在同步方法中,先執行 Update 再 SaveChanges ,也是沒有任何問題的
1.5 同步 SaveChanges
[
]
public void Put([FromBody] TopicViewModel model)
{
var topic = this.context.Topics.Where(f => f.Id == model.Id).FirstOrDefault();
topic.Content = model.Content;
this.context.Update(topic);
Console.WriteLine("Updated");
var affrows = this.context.SaveChanges();
Console.WriteLine("affrows:{0}", affrows);
}
-
輸出結果
從輸出結果可知,先執行了 Update,然後執行了 SaveChanges 輸出 affrows,最後執行了 Dispose 方法
2、問題所在
那到底是什麼問題引起了程式執行的不確定性呢,答案就是 async/await,我們先來嘗試改進一下最初的程式碼
2.1 改進後的程式碼
[
]
public async Task Put([FromBody] TopicViewModel model)
{
var topic = this.context.Topics.Where(f => f.Id == model.Id).FirstOrDefault();
topic.Content = model.Content;
this.context.Update(topic);
Console.WriteLine("Updated");
var affrows = await this.context.SaveChangesAsync();
Console.WriteLine("affrows:{0}", affrows);
}
細心的你已經發現,這段程式碼和 1.1 之中的沒有太多的不同,無非是增加了一些跟蹤資訊,其中,最關鍵的是:增加了傳回值為:Task ,替換了 void
2.2 再次執行修正的程式
輸出結果和 1.5 中的同步方法完全相同,至此,問題解決
3、問題的解決方案
3.1 問題分析
為什麼會發生這種問題呢,原因就是因為使用了非同步方法 async/await 時,當沒有值需要傳回時,使用了 void 造成的,正確的做法是如果沒有傳回值,則傳回 Task,如果有傳回值,則使用 Task
;當一個非同步方法內部沒有傳回 Task 的時候,基於任務的非同步樣式(TAP)並不知道非同步任務的狀態,當 this.context.Update 執行完成後,發現掛載在記憶體中的連線已經沒有使用,就執行了回收;實際上,此時程式還沒有執行完成,但是 TAP 並不知道,所以它不會去阻止這個回收的過程(使用標記),所以 async/await 應該成對出現,並且應該始終傳回 Task 或者 Task ,以確保 TAP 能夠將背景關係進行正確的掛載,否則,當異常發生時,TAP 無非將異常資訊掛載到相應的 Task 上,亦無法跟蹤其執行狀態等資訊
3.2 解決方案
請牢記下麵的鐵律
-
3.2.1 在 EFCore 中,應當始終發揮 AutoDetectChangesEnabled 的特性,不要再更新物體的時候去呼叫 Update 方法
-
3.2.2 使用 async/await 修飾方法時,應該始終傳回 Task 或者 Task
-
適當的使用同步方法,可避免非同步踩坑
演示程式碼下載
https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.TaskThird