DbContext线程安全吗?

66

我想知道DbContext类是否线程安全,我的应用程序正在执行并行线程,并访问DbContext,导致出现大量锁定异常和其他可能与线程有关的问题。因此,我认为它不是线程安全的。

最近我没有得到任何错误提示...但最近我开始在线程中访问DbContext

如果我是正确的,人们会建议什么解决方案?

3个回答

73

它不是线程安全的。只需在您的线程中创建一个新的 DbContext 实例。


3
这也是我的做法。但是,这样做有多安全(不是针对线程,而是数据库)?正如克里斯在他的回答中所说,我应该预料到任何奇怪的行为吗? - Thanasis Ioannidis
我可以在多个线程中使用dbContext,然后在父线程中提交更改吗? @DanielB - Peter Isaac

27

不,EF不是线程安全的——整个EF都不是线程安全的,因为EF上下文永远不应该被共享。


2
那么,每次使用时都应该创建dbcontext?目前,我只是将dbcontext字段作为存储库类的一部分,我想在需要时创建它可以解决这个问题... - jcvandan
2
如果单个逻辑事务使用新的存储库实例并且您的存储库是线程安全的,则可以每个存储库拥有一个上下文。 - Ladislav Mrnka
我猜,为了保存对DbContext的更改,你必须在进行更改的同一实例上调用SaveChanges方法,是吗? - jcvandan
你可以不这样做,但就得处理那些被分离的实体,可能会很棘手。你必须从第一个上下文中分离实体(这会破坏关联),将它们附加到新的上下文中,并告诉新的上下文你所做的更改。 - Ladislav Mrnka
1
@LadislavMrnka,你对锁定有什么看法?我在我的MVC应用程序中每个HTTP请求共享一个DbContext实例,而我们有几个线程在一个实例上工作。 - tugberk
1
由于多个线程访问一个 dbcontext 不是线程安全的,因此您必须锁定多个读/写访问。 - HelloWorld

16

编辑 - 以下是旧答案。

现在我总是使用带有DbContext的这种模式:

using(var db = new LogDbContext())
{
    // Perform work then get rid of the thing
}

我的“每个请求一个线程”的方法意味着在DbContext中缓存的对象会存在并变得陈旧,即使其他DbContext实例正在向其后面的实际数据库写入新值。这会导致一些奇怪的问题,例如一个请求执行插入,而下一个请求在不同线程上进来时会有一个缓存的、陈旧的数据列表。

有一些方法可以使以下工作正常,并提高多读/少写类型应用程序的性能,但它们需要比以上更简单的模式更多的设计和策略。

更新

我还使用了一个有用的辅助方法来处理库方法,例如日志调用。这是辅助方法:

    public static async Task Using(Db db, Func<Db, Task> action)
    {
        if (db == null)
        {
            using (db = new Db())
            {
                await action(db);
            }
        }
        else
        {
            await action(db);
        }
    }

使用这个方法,我可以轻松编写代码,根据调用方式的不同,接受一个可选的现有DbContext,或者在using上下文中实例化一个DbContext。

例如,在使用DbContext时,我可能会加载一些数据,记录一些信息,然后保存这些数据——从性能的角度来看,最好使用同一个DbContext完成所有操作。另一方面,我可能还想在响应简单操作时记录某些内容,既不加载也不写入任何其他数据。通过利用上述方法,我可以只有一个日志记录方法,无论您是要在现有的DbContext中工作还是不需要DbContext,都可以使用该方法:

public async Task WriteLine(string line, Db _db = null)
{
    await Db.Using(_db, db => {
        db.LogLines.Add(new LogLine(line));
        await db.SaveChangesAsync();
    });
}

现在这个方法可以在现有的DbContext内外被调用,并且仍然能够正确地运行,而不需要拥有两个版本的每个方便日志记录方法或其他实用方法,更不需要知道并计划每个调用它们或其调用者的上下文。这基本上为我带来了以下线程静态策略的一个好处,即我不必担心实用程序调用中数据库何时打开的问题。

旧答案

我通常像这样处理EF DbContext的线程安全:

public class LogDbContext : DbContext
{
    . . .

    [ThreadStatic]
    protected static LogDbContext current;

    public static LogDbContext Current()
    {
        if (current == null)
            current = new LogDbContext();

        return current;
    }

    . . .
}

有了这个东西,我可以像这样为该线程获取一个 DbContext:

var db = LogDbContext.Current();

需要注意的是,由于每个DbContext都有自己的本地缓存,因此每个线程现在将拥有其自己独立的实体对象缓存,如果您没有准备好应对这种情况,则可能会引入一些奇怪的行为。但是创建新的DbContext对象可能是昂贵的,而此方法最小化了这种成本。


我以同样的方式处理这种情况。但由于我是EF新手,您能否详细说明一下您所说的那种疯狂行为呢? 我在Web Forms项目中使用此dbcontext,并在Application_EndRequest事件中处理每个线程的当前dbcontext。 - Thanasis Ioannidis
1
Application_EndRequest不是Dispose它的正确位置,因为请求不一定会从头到尾在该线程上处理;在此完成之前,另一个请求可能会在同一线程上处理,Dispose DbContext,然后BOOM。这种疯狂的行为太长了,无法在评论中描述,请发布单独的问题,我会在那里回答。 - Chris Moschini
1
奇怪,我以为每个请求都有自己的线程!那么,在WebForms页面生命周期中,正确的位置在哪里处置线程静态对象呢? - Thanasis Ioannidis
1
请求在ASP.Net线程池上处理,如果你搜索一下,你会发现关于这个主题的无尽文章。基本上,许多请求在任何给定的线程上被处理。我不会处理我的DbContext;它自己管理打开和关闭连接。 - Chris Moschini
@ChrisMoschini 意识到这是一个旧的线程,这也是它变得有趣的部分。但是,正如你自己所指出的那样,旧的解决方案似乎是针对每个线程而不是每个请求的。这似乎是导致你遇到问题的原因。了解你今天在这篇文章中写的内容会很有趣。如果你仍然使用原子上下文范围(我个人认为这非常麻烦,特别是当涉及到具有存储库或查询对象的多层应用程序时)。 - Base
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接