先看這一段異常信息:
A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
不要被提示信息中的 Use 'await' 所迷惑,如果你仔細查看下代碼,發現并沒有什么問題,上面這段異常信息,是我們在 async/await 操作的時候經常遇到的,什么意思呢?我們分解下:
- A second operation started on this context before a previous asynchronous operation completed. :在這個上下文,第二個操作開始于上一個異步操作完成之前。可能有點繞,簡單說就是,在同一個上下文,一個異步操作還沒完成,另一個操作就開始了。
- Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. :在這個上下文,使用 await 來確保所有的異步操作完成于另一個方法調用之前。
- Any instance members are not guaranteed to be thread safe.:所有實例成員都不能保證是線程安全的。
什么是線程安全呢?
- 線程安全,指某個函數、函數庫在多線程環境中被調用時,能夠正確地處理各個線程的局部變量,使程序功能正確完成。(來自維基百科)
DbContext 是不是線程安全的呢?
- The context is not thread safe. You can still create a multithreaded application as long as an instance of the same entity class is not tracked by multiple contexts at the same time.(來自 MSDN)
我們來解析這段話,首先,DbContext 不是線程安全的,也就是說,你在當前線程中,只能創建一個 DbContext 實例對象(特定情況下),并且這個對象并不能被共享,后面那句話是什么意思呢?注意其中的關鍵字,不被追蹤的實體類,在同一時刻的多線程應用程序中,可以被多個上下文創建,不被追蹤是什么意思呢?可以理解為不被修改的實體,通過這段代碼獲取:context.Entry(entity).State
。
我們知道 DbContext 就像一個大的數據容器,通過它,我們可以很方便的進行數據查詢和修改,在之前的一篇博文中,有一段 EF DbContext SaveChanges 的源碼:
[DebuggerStepThrough]
public virtual int SaveChanges(bool acceptAllChangesOnSuccess)
{
var entriesToSave = Entries
.Where(e => e.EntityState == EntityState.Added
|| e.EntityState == EntityState.Modified
|| e.EntityState == EntityState.Deleted)
.Select(e => e.PrepareToSave())
.ToList();
if (!entriesToSave.Any())
{
return 0;
}
try
{
var result = SaveChanges(entriesToSave);
if (acceptAllChangesOnSuccess)
{
AcceptAllChanges(entriesToSave);
}
return result;
}
catch
{
foreach (var entry in entriesToSave)
{
entry.AutoRollbackSidecars();
}
throw;
}
}
在 DbContext 執行 AcceptAllChanges 之前,會檢測實體狀態的改變,所以,SaveChanges 會和當前上下文一一對應,如果是同步方法,所有的操作都是等待,這是沒有什么問題的,但試想一下,如果是異步多線程,當一個線程創建 DbContext 對象,然后進行一些實體狀態修改,在還沒有 AcceptAllChanges 執行之前,另一個線程也進行了同樣的操作,雖然第一個線程可以 SaveChanges 成功,但是第二個線程肯定會報錯,因為實體狀態已經被另外一個線程中的 DbContext 應用了。
在多線程調用時,能夠正確地處理各個線程的局部變量,使程序功能正確完成,這是線程安全,但顯然 DbContext 并不能保證它一定能正確完成,所以它不是線程安全,MSDN 中的說法:Any public static members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
下面我們做一個測試,測試代碼:
using (var context = new TestDbContext2())
{
var clients = await context.Clients.ToListAsync();
var servers = await context.Servers.ToListAsync();
}
上面代碼是我們常寫的,一個 DbContext 下可能有很多的操作,測試結果也沒什么問題,我們接著再修改下代碼:
using (var context = new TestDbContext2())
{
var clients = context.Clients.ToListAsync();
var servers = context.Servers.ToListAsync();
await Task.WhenAll(clients, servers);
}
Task.WhenAll 的意思是將所有等待的異步操作同時執行,執行后你會發現,會時不時的報一開始的那個錯誤,為什么這樣會報錯?并且還是時不時的呢?我們先分析下上面兩段代碼,有什么不同,其實都是異步,只是下面的同時執行異步方法,但并不是絕對同時,所以會時不時的報錯,根據一開始對 DbContext 的分析,和上面的測試,我們就明白了:同一時刻,一個上下文只能執行一個異步方法,第一種寫法其實也會報錯的,但幾率非常非常小,可以忽略不計,第二種寫法我們只是把這種幾率提高了,但也并不是絕對。
還有一種情況是,如果項目比較復雜,我們會一般會設計基于 DbContext 的 UnitOfWork,然后在項目開始的時候,進行 IoC 注入映射類型,比如下面這段代碼:
UnityContainer container = new UnityContainer();
container.RegisterType<IUnitOfWork, UnitOfWork>(new PerResolveLifetimeManager());
除了映射類型之外,我們還會對 UnitOfWork 對象的生命周期進行管理,PerResolveLifetimeManager 的意思是每次請求進行解析對象,也就是說每次請求下,UnitOfWork 是唯一的,只是針對當前請求,為什么要這樣設計?一方面為了共享 IUnitOfWork 對象的注入,比如 Application 中會對多個 Repository 進行操作,但現在我覺得,還有一個好處是減少線程安全錯誤幾率的出現,因為之前說過,多線程情況下,一個線程創建 DbContext,然后進行修改實體狀態,在應用更改之前,另一個線程同時創建了 DbContext,并也修改了實體狀態,這時候,第一個線程創建的 DbContext 應用更改了,第二個線程創建的 DbContext 應用更改就會報錯,所以,一個解決方法就是,減少 DbContext 的創建,比如,上面一個請求只創建一個 DbContext。
因為 DbContext 不是線程安全的,所以我們在多線程應用程序運用它的時候,要注意下面兩點:
- 同一時刻,一個上下文只能執行一個異步方法。
- 實體狀態改變,對應一個上下文,不能跨上下文修改實體狀態,也不能跨上下文應用實體狀態。
異步下使用 DbContext,我個人覺得,不管代碼怎么寫,還是會報線程安全的錯誤,只不過這種幾率會很小很小,可能應用程序運行了幾年,也不會出現一次錯誤,但出錯幾率會隨著垃圾代碼和高并發,慢慢會提高上來。
參考資料:
文章列表