EntityFramework DbContext 线程安全

先看这一段异常信息:

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]publicvirtualintSaveChanges(boolacceptAllChangesOnSuccess){varentriesToSave = Entries        .Where(e => e.EntityState == EntityState.Added                    || e.EntityState == EntityState.Modified                    || e.EntityState == EntityState.Deleted)        .Select(e => e.PrepareToSave())        .ToList();if(!entriesToSave.Any())    {return0;    }try{varresult = SaveChanges(entriesToSave);if(acceptAllChangesOnSuccess)        {            AcceptAllChanges(entriesToSave);        }returnresult;    }    catch    {foreach(varentryinentriesToSave)        {            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(varcontext =newTestDbContext2()){varclients =awaitcontext.Clients.ToListAsync();varservers =awaitcontext.Servers.ToListAsync();}

上面代码是我们常写的,一个 DbContext 下可能有很多的操作,测试结果也没什么问题,我们接着再修改下代码:

using(varcontext =newTestDbContext2()){varclients = context.Clients.ToListAsync();varservers = context.Servers.ToListAsync();awaitTask.WhenAll(clients, servers);}

Task.WhenAll 的意思是将所有等待的异步操作同时执行,执行后你会发现,会时不时的报一开始的那个错误,为什么这样会报错?并且还是时不时的呢?我们先分析下上面两段代码,有什么不同,其实都是异步,只是下面的同时执行异步方法,但并不是绝对同时,所以会时不时的报错,根据一开始对 DbContext 的分析,和上面的测试,我们就明白了:同一时刻,一个上下文只能执行一个异步方法,第一种写法其实也会报错的,但几率非常非常小,可以忽略不计,第二种写法我们只是把这种几率提高了,但也并不是绝对。

还有一种情况是,如果项目比较复杂,我们会一般会设计基于 DbContext 的 UnitOfWork,然后在项目开始的时候,进行 IoC 注入映射类型,比如下面这段代码:

UnityContainer container =newUnityContainer();container.RegisterType(newPerResolveLifetimeManager());

除了映射类型之外,我们还会对 UnitOfWork 对象的生命周期进行管理,PerResolveLifetimeManager 的意思是每次请求进行解析对象,也就是说每次请求下,UnitOfWork 是唯一的,只是针对当前请求,为什么要这样设计?一方面为了共享 IUnitOfWork 对象的注入,比如 Application 中会对多个 Repository 进行操作,但现在我觉得,还有一个好处是减少线程安全错误几率的出现,因为之前说过,多线程情况下,一个线程创建 DbContext,然后进行修改实体状态,在应用更改之前,另一个线程同时创建了 DbContext,并也修改了实体状态,这时候,第一个线程创建的 DbContext 应用更改了,第二个线程创建的 DbContext 应用更改就会报错,所以,一个解决方法就是,减少 DbContext 的创建,比如,上面一个请求只创建一个 DbContext。

因为 DbContext 不是线程安全的,所以我们在多线程应用程序运用它的时候,要注意下面两点:

同一时刻,一个上下文只能执行一个异步方法。

实体状态改变,对应一个上下文,不能跨上下文修改实体状态,也不能跨上下文应用实体状态。

异步下使用 DbContext,我个人觉得,不管代码怎么写,还是会报线程安全的错误,只不过这种几率会很小很小,可能应用程序运行了几年,也不会出现一次错误,但出错几率会随着垃圾代码和高并发,慢慢会提高上来。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容