標籤:mic scope 方法調用 prepare 追蹤 back operation nbsp 一段
from:http://www.cnblogs.com/xishuai/p/ef-dbcontext-thread-safe.html
先看這一段異常資訊:
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,我個人覺得,不管代碼怎麼寫,還是會報安全執行緒的錯誤,只不過這種幾率會很小很小,可能應用程式運行了幾年,也不會出現一次錯誤,但出錯幾率會隨著垃圾代碼和高並發,慢慢會提高上來。
參考資料:
- Managing DbContext the right way with Entity Framework 6: an in-depth guide
- Multi-async in Entity Framework 6?
- Entity Framework Working with DbContext
- Entity framework async issues context or query?
- Is DbContext thread safe?
- DataContext 是否可以靜態化?
- EntityFramework 用法探索(七)安全執行緒實踐
EntityFramework DbContext 安全執行緒