我们在设计多线程实体框架驱动的应用程序时遇到一些麻烦,并希望得到一些指导。我们在不同的线程上创建实体,将实体添加到集合中,然后将数据绑定到各种wpf控件。 ObjectContext类不是线程安全的,因此管理它我们基本上有两个解决方案:
解决方案1具有单个上下文并小心使用锁定以确保没有2个线程同时访问它。这将是相对简单的实现,但需要在应用程序的持续时间内保持上下文。像这样打开一个上下文实例是不是一个坏主意?
解决方案2是按需创建上下文对象,然后立即分离对象,然后将它们保存在我们自己的集合中,然后重新附加它们以进行任何更新。但是这有一些严重的问题,因为当分离对象时,它们会丢失对导航属性对象的引用。还有一个问题是,2个线程仍然可以尝试访问单个对象,并且都尝试将()附加到上下文。此外,每次我们想要访问实体导航属性时,我们都需要提供新的上下文。
问:两种解决方案中的任何一种都有效,如果不是,您如何建议我们解决这个问题?
答案 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);
}
关键是转到数据源并避免使用数据模型。