博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
EntityFramework DbContext 线程安全
阅读量:6606 次
发布时间:2019-06-24

本文共 3847 字,大约阅读时间需要 12 分钟。

不要被提示信息中的 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
(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,如需转载请自行联系原作者

你可能感兴趣的文章
[Android组件化]组件化数据分享
查看>>
你必须知道的HTTP基本概念
查看>>
当下拉列表数据过大时,该如何应对?
查看>>
使用OpenGrok搭建 可搜索可跳转的源码 阅读网站
查看>>
Android ContentProvider调用报错"Bad call:..."及相关Binder权限问题分析
查看>>
配置通过VLANIF实现跨设备VLAN内通信
查看>>
Linux-正则表达式
查看>>
基本shell脚本的编辑及变量
查看>>
加密和解密 tar
查看>>
[李景山php]每天TP5-20161216|thinkphp5-helper.php-1
查看>>
VMware、Workstation 使用
查看>>
用户输入和while循环
查看>>
将datatable 保存为 Excel文件(高效率版本)
查看>>
C/C++五大内存分区(转)
查看>>
System V 共享内存区
查看>>
springmvc_1(hello world)
查看>>
0.随笔——读后感
查看>>
StringUtils类方法解析
查看>>
CentOS 6.5下PXE+Kickstart无人值守安装操作系统
查看>>
Nginx ssl/https 配置
查看>>