实体框架和多线程

时间:2012-02-01 16:23:46

标签: .net multithreading frameworks entity

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

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

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

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

6 个答案:

答案 0 :(得分:27)

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

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

解决方案#2对我来说似乎行不通 - 在整个应用程序中,人们忘记重新连接(或分离)实体时,你最终会遇到微妙的错误。

一种解决方案是不在应用程序的UI层中使用您的实体对象。无论如何,我建议这样做 - 实体对象的结构/布局不是你想要在用户界面上显示东西的最佳方式(这就是MVC模式族的原因)。您的DAL应该具有特定于业务逻辑的方法(例如UpdateCustomer),并且它应该在内部决定是创建新的Context还是使用存储的Context。您可以从单个存储的上下文方法开始,然后如果遇到瓶颈问题,您需要进行更改的表面区域有限。

缺点是您需要编写更多代码 - 您拥有EF实体,但是您还拥有具有重复属性且可能与许多EF实体的基数不同的业务实体。为了缓解这种情况,您可以使用AutoMapper之类的框架来简化从EF实体到业务实体的属性复制,然后重新开始。

答案 1 :(得分:11)

我似乎有十几个关于EF和多线程的stackoverflow线程。所有这些都有答案可以深入解释问题,但并没有真正告诉你如何解决它。

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...
}

答案 2 :(得分:0)

你不想要一个长期存在的背景。理想情况下,它们应该用于请求/数据操作的生命周期。

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

性能会受到一些打击,但它提供了一种防弹方式,可以确保导航属性不会因为“忘记”它们而受到损坏。

答案 3 :(得分:0)

自提出问题以来已经过了一段时间,但我最近遇到了类似的问题,最后做了以下工作,这有助于我们达到绩效标准。

您基本上将列表拆分为块,并以多线程方式在sperate线程中处理它们。每个新线程也会启动他们自己的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);
                    }
                });
    }

答案 4 :(得分:0)

我在DbContext中使用Blazor服务器端。

我实际上已经完成了您的第二种方法,并且一切正常。小心未追踪的实体。

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

此外,所有查询都使用“ AsNoTracking”来防止在缓存中保存上一个查询的记录。

此后,所有写入/更新/删除更新均由唯一的DbContext进行。

它变成了这样的东西:

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();
        }
    }
}

答案 5 :(得分:-1)

我刚刚有一个项目尝试使用多线程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);
}

关键是转到数据源并避免使用数据模型。