EntityFramework DbContext 线程安全 - 田园里的蟋蟀 - 博客园

标签: | 发表时间:2018-12-18 10:56 | 作者:
出处:https://www.cnblogs.com

先看这一段异常信息:

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 classis not trackedby 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,我个人觉得,不管代码怎么写,还是会报线程安全的错误,只不过这种几率会很小很小,可能应用程序运行了几年,也不会出现一次错误,但出错几率会随着垃圾代码和高并发,慢慢会提高上来。

参考资料:

相关 [entityframework dbcontext 线程安全] 推荐:

EntityFramework DbContext 线程安全 - 田园里的蟋蟀 - 博客园

- -
不要被提示信息中的 Use 'await' 所迷惑,如果你仔细查看下代码,发现并没有什么问题,上面这段异常信息,是我们在 async/await 操作的时候经常遇到的,什么意思呢. :在这个上下文,第二个操作开始于上一个异步操作完成之前. 可能有点绕,简单说就是,在同一个上下文,一个异步操作还没完成,另一个操作就开始了.

什么是线程安全

- - CSDN博客编程语言推荐文章
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题. 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源. 如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件. 实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的.

Servlet是否线程安全

- - 研发管理 - ITeye博客
Servlet是线程安全吗. 要解决这个问题,首先要知道什么是线程安全:.   如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码. 如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题.

web开发中的线程安全

- - 编程 - 编程语言 - ITeye博客
在web开发中,要关注由于并发访问所导致的对某一同一个值的修改,否则信息会造成泄漏servlet是在多线程环境下的. 即可能有多个请求发给一个servelt实例,每个请求是一个线程. struts下的action也类似,同样在多线程环境下. 译:为多线程环境编写代码. 我们的controller servlet指挥创建你的Action 类的一个实例,用此实例来服务所有的请求.

Spring并发访问的线程安全性问题

- - 寒江孤影
和Struts一样,Spring的Controller默认是Singleton的,这意味着每个request过来,系统都会用原有的instance去处理,这样导致了两个结果:一是我们不用每次创建Controller,二是减少了对象创建和垃圾收集的时间;由于只有一个Controller的instance,当多个线程调用它的时候,它里面的instance变量就不是线程安全的了,会发生窜数据的问题.

Spring单实例、多线程安全、事务解析

- - zzm
 在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:.     DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了.

APP 缓存数据线程安全问题探讨

- - bang’s blog
一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取. 我们先忽略从网络请求数据这一环节,假设数据已经保存在 DB 里,那我们要做的事就是,ViewController 从 DB 取数据,再传给 view 渲染:.

Spring单例Bean和线程安全 - duanxz - 博客园

- -
Spring的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢. 例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例的,那么对于Spring托管的单例Service Bean,如何保证其安全呢.

设计高效的线程安全的缓存--JCIP5.6读书笔记

- - ITeye博客
[本文是我对Java Concurrency In Practice 5.6的归纳和总结.  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. 几乎每一个应用都会使用到缓存, 但是设计高效的线程安全的缓存并不简单. // 使用synchronized同步整个方法解决线程安全. Memorizer1使用HashMap缓存计算结果.

获得更好并发和线程安全型的场景和解决方案

- - ITeye博客
               你需要从数据库加载股票交易的安全代码并且考虑到性能进行了缓存. 这些安全代码需要每30分钟刷新一次. 在这里缓存数据需要一个单独的写线程进行生成和刷新,并且被若干读进程进行读取. 这种情况下,你又要如何来保证你的读写方案做到良好的扩展和线程安全呢.                     解决方案:java.util.concurrent.locks包提供了可以并行执行的写锁以及被单个线程独占的写锁的实现.