实体框架和多线程

Posted

技术标签:

【中文标题】实体框架和多线程【英文标题】:Entity Framework and Multi threading 【发布时间】:2012-02-24 08:12:19 【问题描述】:

我们在设计多线程实体框架驱动的应用程序时遇到了一些问题,希望得到一些指导。我们在不同的线程上创建实体,将实体添加到集合中,然后将数据绑定到各种 WPF 控件。 ObjectContext 类不是线程安全的,因此我们基本上有两种解决方案来管理它:

解决方案 1 具有单一上下文,并小心使用锁定以确保没有 2 个线程同时访问它。这实现起来相对简单,但需要上下文在应用程序期间处于活动状态。像这样打开单个上下文实例是不是一个坏主意?

解决方案 2 是按需创建上下文对象,然后立即分离对象,然后将它们保存在我们自己的集合中,然后重新附加它们以进行任何更新。但是,这有一些严重的使用问题,因为当对象被分离时,它们会丢失对导航属性对象的引用。还有一个问题是 2 个线程仍然可以尝试访问单个对象,并且都尝试将其附加()到上下文。此外,每次我们想要访问实体的导航属性时,我们都需要提供一个新的上下文。

问:这两种解决方案中的任何一种都有效吗?如果无效,您建议我们如何解决这个问题?

【问题讨论】:

@usr 你有更好的主意吗? @Cocowalla 不知道 OP 正在解决的更大场景,我不知道。他的两种解决方案都将导致痛苦的实施,这就是我警告他的原因。也许他可以走一条完全不同的路,以单线程的方式使用 EF(它应该被使用的方式)。 另外需要注意的是:当实体分离时,您不能对实体进行任何更改,因为当前没有上下文跟踪该更改。稍后调用 SaveChanges() 时,更改将不会保留。 另外,一个有时有用的技巧(有点)需要注意:如果你没有显式调用 Detach(),那么即使在上下文被释放后,实体也会保留其导航属性。可以证明有用:) 两个快速建议,1 - 断开连接 2 - 远离 LazyLoading 【参考方案1】:

首先,我假设您已经阅读了 MSDN 上的文章 "Multithreading and the Entity Framework"。

从线程的角度来看,解决方案#1 几乎可以肯定是最安全的,因为您保证在任何给定时间只有一个线程与上下文交互。保持上下文并没有本质上的错误——它不会在幕后保持数据库连接打开,所以它只是内存开销。当然,如果您最终在该线程上遇到瓶颈并且整个应用程序是使用单数据库线程假设编写的,那么这可能会出现性能问题。

解决方案 #2 对我来说似乎行不通 - 您最终会在整个应用程序中出现一些细微的错误,导致人们忘记重新附加(或分离)实体。

一种解决方案是不在应用程序的 UI 层中使用您的实体对象。无论如何,我都会推荐这个——很可能,实体对象的结构/布局对于您希望在用户界面上显示事物的方式并不是最佳的(这就是MVC 模式系列的原因)。您的 DAL 应该具有特定于业务逻辑的方法(例如UpdateCustomer),并且它应该在内部决定是创建新上下文还是使用存储的上下文。您可以从单一存储上下文方法开始,然后如果遇到瓶颈问题,您需要进行更改的表面积有限。

缺点是您需要编写更多代码 - 您将拥有 EF 实体,但您也会拥有具有重复属性且许多 EF 实体可能具有不同基数的业务实体。为了缓解这种情况,您可以使用 AutoMapper 之类的框架来简化将属性从 EF 实体复制到业务实体并再次复制回来的过程。

【讨论】:

克里斯,感谢您的回答,这对澄清我们的想法非常有帮助。我们将接受您的建议,我认为我们对尝试使用我们的实体对象来做所有事情有点太过分了。 解决方案 1 的另一个问题是缓存:上下文将缓存实体,因此当在应用程序的运行实例之外修改数据时,数据将变得陈旧 您提供的链接让我了解了 EF 的工作原理。非常感谢。【参考方案2】:

我似乎有十几个关于 EF 和多线程的 *** 线程。他们都有深入解释问题的答案,但并没有真正向您展示如何解决它。

EF 不是线程安全的,我们现在都知道了。但以我的经验,唯一的风险是上下文创建/操作。 实际上有一个非常简单的解决方法,您可以保持延迟加载。

假设您有一个 WPF 应用程序和一个 MVC 网站。 WPF 应用程序使用多线程的地方。您只需在多线程中处理 db 上下文并在不使用时保留它。以 MVC 网站为例,在呈现视图后上下文将自动释放。

在 WPF 应用程序层中使用:

ProductBLL productBLL = new ProductBLL(true);

在 MVC 应用层你使用这个:

ProductBLL productBLL = new ProductBLL();

您的产品业务逻辑层应如下所示:

public class ProductBLL : IProductBLL

    private ProductDAO productDAO; //Your DB layer

    public ProductBLL(): this(false)
    

    
    public ProductBLL(bool multiThreaded)
    
        productDAO = new ProductDAO(multiThreaded);
    
    public IEnumerable<Product> GetAll()
    
        return productDAO.GetAll();
    
    public Product GetById(int id)
    
        return productDAO.GetById(id);
    
    public Product Create(Product entity)
    
        return productDAO.Create(entity);
    
    //etc...

您的数据库逻辑层应如下所示:

public class ProductDAO : IProductDAO

    private YOURDBCONTEXT db = new YOURDBCONTEXT ();
    private bool _MultiThreaded = false;

    public ProductDAO(bool multiThreaded)
    
        _MultiThreaded = multiThreaded;
    
    public IEnumerable<Product> GetAll()
    
        if (_MultiThreaded)
        
            using (YOURDBCONTEXT  db = new YOURDBCONTEXT ())
            
                return db.Product.ToList(); //USE .Include() For extra stuff
            
        
        else
        
            return db.Product.ToList();
                          
    

    public Product GetById(int id)
    
        if (_MultiThreaded)
        
            using (YOURDBCONTEXT  db = new YOURDBCONTEXT ())
            
                return db.Product.SingleOrDefault(x => x.ID == id); //USE .Include() For extra stuff
            
        
        else
        
            return db.Product.SingleOrDefault(x => x.ID == id);
                  
    

    public Product Create(Product entity)
    
        if (_MultiThreaded)
        
            using (YOURDBCONTEXT  db = new YOURDBCONTEXT ())
            
                db.Product.Add(entity);
                db.SaveChanges();
                return entity;
            
        
        else
        
            db.Product.Add(entity);
            db.SaveChanges();
            return entity;
        
    

    //etc...

【讨论】:

【参考方案3】:

您不想要一个长期存在的上下文。理想情况下,它们应该在请求/数据操作的整个生命周期内。

在处理类似问题时,我最终实现了一个存储库,该存储库为给定类型缓存 PK 实体,并允许“LoadFromDetached”在数据库中查找实体,并“复制”除 PK 之外的所有标量属性到新附加的实体。

性能会受到一些影响,但它提供了一种防弹的方法来确保导航属性不会因“忘记”它们而受到破坏。

【讨论】:

我最近遇到了类似类型的问题,结果发现性能提高了很多。输入错误的地方,看看我的答案【参考方案4】:

自从提出问题以来已经有一段时间了,但我最近遇到了一个类似类型的问题,并最终做了以下帮助我们满足性能标准的问题。

您基本上将您的列表拆分为多个块,并以多线程方式在单独的线程中处理它们。每个新线程还会启动它们自己的 uow,这需要附加您的实体。

需要注意的一点是您的数据库需要启用快照隔离;否则你可能会陷入死锁。您需要确定这对于您正在执行的操作和相关业务流程是否可行。在我们的例子中,它是对产品实体的简单更新。

您可能需要进行一些测试来确定最佳块大小并限制并行度,以便始终有资源来完成操作。

    private void PersistProductChangesInParallel(List<Product> products, 
        Action<Product, string> productOperationFunc, 
        string updatedBy)
    
        var productsInChunks = products.ChunkBy(20);

        Parallel.ForEach(
            productsInChunks,
            new ParallelOptions  MaxDegreeOfParallelism = 20 ,
            productsChunk =>
                
                    try
                    
                        using (var transactionScope = new TransactionScope(
                                TransactionScopeOption.Required,
                                new TransactionOptions  IsolationLevel = IsolationLevel.Snapshot ))
                        
                            var dbContext = dbContextFactory.CreatedbContext();
                            foreach (var Product in productsChunk)
                            
                                dbContext.products.Attach(Product);
                                productOperationFunc(Product, updatedBy);
                            
                            dbContext.SaveChanges();
                            transactionScope.Complete();
                        
                    
                    catch (Exception e)
                    
                        Log.Error(e);
                        throw new ApplicationException("Some products might not be updated", e);
                    
                );
    

【讨论】:

【参考方案5】:

我正在使用带有 DbContext 的 Blazor 服务器端。

我实际上已经完成了您的第二种方式,并且效果很好。小心未跟踪的实体。

一个作用域的 DbContext,它是一个实现 IReadOnlyApplicationDbContext 的接口,它只为 DbSets 提供 IQueryable(没有 SaveChanges 或类似的东西)。因此,所有读取操作都可以安全执行,而不会出现更新弄乱数据的问题。

此外,所有查询都使用“AsNoTracking”来防止将最后一个查询的轨迹保存在缓存中。

之后,所有的写入/更新/删除更新都由唯一的 DbContexts 进行。

变成了这样:

public interface IReadOnlyApplicationDbContext

    DbSet<Product> Products  get; 


public interface IApplicationDbContext : IReadOnlyDbContext

    DbSet<Product> Products  set; 


public class ApplicationDbContext : DbContext, IApplicationDbContext

    DbSet<Product> Products  get; set; 


public abstract class ProductRepository

    private readonly IReadOnlyApplicationDbContext _readOnlyApplicationDbContext;
    private readonly IFactory<IApplicationDbContext> _applicationDbContextFactory;
    
    protected Repository(
        IReadOnlyApplicationDbContext readOnlyApplicationDbContext,
        IFactory<IApplicationDbContext> applicationDbContextFactory
    )
    
        _readOnlyApplicationDbContext = readOnlyApplicationDbContext;
        _applicationDbContextFactory = _applicationDbContextFactory;
    
    
    private IQueryable<Product> ReadOnlyQuery() => _readOnlyApplicationDbContext.AsNoTracking();
    
    public Task<IEnumerable<Products>> Get()
    
        return ReadOnlyQuery().Where(s=>s.SKU == "... some data ...");
    
    
    public Task Update(Product product)
    
        using (var db = _applicationDbContextFactory.Create())
        
            db.Entity(product).State = EntityState.Modified;
            return db.SaveChangesAsync();
        
    
    
    public Task Add(Product product)
    
        using (var db = _applicationDbContextFactory.Create())
        
            db.Products.AddAsync(product);
            return db.SaveChangesAsync();
        
    

【讨论】:

【参考方案6】:

我刚刚有一个项目,尝试将 EF 与多线程一起使用会导致错误。

我试过了

using (var context = new entFLP(entity_connection))            

    context.Product.Add(entity);
    context.SaveChanges();
    return entity;

但它只是将错误的类型从 datareader 错误更改为多线程错误。

简单的解决方案是使用带有 EF 函数导入的存储过程

using (var context = new entFLP(entity_connection))

    context.fi_ProductAdd(params etc);

关键是去数据源头,避开数据模型。

【讨论】:

以上是关于实体框架和多线程的主要内容,如果未能解决你的问题,请参考以下文章

实体框架和多线程

多进程和多线程有啥区别?

线程的概念和多线程模子

python多线程和多进程的实现

python多线程和多进程的实现

Java 线程池和多线程编程 ——线程池理解与创建