EntityFrameworkDbContext執行緒安全

範大腳腳發表於2017-11-15

不要被提示資訊中的 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,我個人覺得,不管程式碼怎麼寫,還是會報執行緒安全的錯誤,只不過這種機率會很小很小,可能應用程式執行了幾年,也不會出現一次錯誤,但出錯機率會隨著垃圾程式碼和高併發,慢慢會提高上來。

本文轉自田園裡的蟋蟀部落格園部落格,原文連結:http://www.cnblogs.com/xishuai/p/ef-dbcontext-thread-safe.html,如需轉載請自行聯絡原作者


相關文章