我有一个ID列表,我需要在每个ID上运行几个存储过程。
当我使用标准的foreach循环时,它工作正常,但是当我有很多记录时,它的工作速度很慢。
我想将代码转换为使用EF,但我得到一个例外:"底层提供程序在Open"上失败。
我在Parallel.ForEach中使用此代码:
using (XmlEntities osContext = new XmlEntities())
{
//The code
}
但它仍然引发了异常。
任何想法我如何使用与EF并行?我是否需要为我正在运行的每个程序创建一个新的上下文?我有大约10个程序,所以我认为创建10个上下文非常糟糕,每个上下文一个。
答案 0 :(得分:35)
The underlying database connections that the Entity Framework are using are not thread-safe。 将需要为您要执行的另一个线程上的每个操作创建一个新的上下文。
您对如何并行化操作的关注是有效的;打开和关闭的许多背景都很昂贵。
相反,您可能想要反思您对并行化代码的看法。看起来你正在循环遍历许多项目,然后为每个项目串行调用存储过程。
如果可以,请为每个过程创建一个新的Task<TResult>
(或Task
,如果您不需要结果),然后在{{1}中创建打开一个上下文,循环遍历所有项,然后执行存储过程。这样,您只有许多上下文等于并行运行的存储过程的数量。
假设您有一个Task<TResult>
,其中包含两个存储过程MyDbContext
和DoSomething1
,这两个存储过程都采用类DoSomething2
的实例。
实现上述内容将类似于:
MyItem
如果不能并行执行存储过程(每个存储过程都依赖于以特定顺序运行),那么您仍然可以并行化您的操作,这只是更复杂一点。
您可以在商品中查看creating custom partitions(使用Create
method上的静态Partitioner
class)。这将为您提供实现IEnumerator<T>
实施的方法(请注意,这不是 IEnumerable<T>
,因此您无法// You'd probably want to materialize this into an IList<T> to avoid
// warnings about multiple iterations of an IEnumerable<T>.
// You definitely *don't* want this to be an IQueryable<T>
// returned from a context.
IEnumerable<MyItem> items = ...;
// The first stored procedure is called here.
Task t1 = Task.Run(() => {
// Create the context.
using (var ctx = new MyDbContext())
// Cycle through each item.
foreach (MyItem item in items)
{
// Call the first stored procedure.
// You'd of course, have to do something with item here.
ctx.DoSomething1(item);
}
});
// The second stored procedure is called here.
Task t2 = Task.Run(() => {
// Create the context.
using (var ctx = new MyDbContext())
// Cycle through each item.
foreach (MyItem item in items)
{
// Call the first stored procedure.
// You'd of course, have to do something with item here.
ctx.DoSomething2(item);
}
});
// Do something when both of the tasks are done.
覆盖它。
对于您回来的每个foreach
实例,您需要创建一个新的IEnumerator<T>
(如果您需要一个结果),并在Task<TResult>
正文中创建上下文然后循环浏览Task<TResult>
返回的项目,按顺序调用存储过程。
看起来像这样:
IEnumerator<T>
答案 1 :(得分:6)
答案 2 :(得分:3)
这是我使用的,效果很好。它还支持处理错误异常,并具有调试模式,可以更容易地跟踪事情
public static ConcurrentQueue<Exception> Parallel<T>(this IEnumerable<T> items, Action<T> action, int? parallelCount = null, bool debugMode = false)
{
var exceptions = new ConcurrentQueue<Exception>();
if (debugMode)
{
foreach (var item in items)
{
try
{
action(item);
}
// Store the exception and continue with the loop.
catch (Exception e)
{
exceptions.Enqueue(e);
}
}
}
else
{
var partitions = Partitioner.Create(items).GetPartitions(parallelCount ?? Environment.ProcessorCount).Select(partition => Task.Factory.StartNew(() =>
{
while (partition.MoveNext())
{
try
{
action(partition.Current);
}
// Store the exception and continue with the loop.
catch (Exception e)
{
exceptions.Enqueue(e);
}
}
}));
Task.WaitAll(partitions.ToArray());
}
return exceptions;
}
您可以像下面这样使用它,因为db是原始的DbContext,db.CreateInstance()使用相同的连接字符串创建一个新实例。
var batch = db.Set<SomeListToIterate>().ToList();
var exceptions = batch.Parallel((item) =>
{
using (var batchDb = db.CreateInstance())
{
var batchTime = batchDb.GetDBTime();
var someData = batchDb.Set<Permission>().Where(x=>x.ID = item.ID).ToList();
//do stuff to someData
item.WasMigrated = true; //note that this record is attached to db not batchDb and will only be saved when db.SaveChanges() is called
batchDb.SaveChanges();
}
});
if (exceptions.Count > 0)
{
logger.Error("ContactRecordMigration : Content: Error processing one or more records", new AggregateException(exceptions));
throw new AggregateException(exceptions); //optionally throw an exception
}
db.SaveChanges(); //save the item modifications
答案 3 :(得分:0)
在不知道内部异常结果是什么(如果有的话)的情况下解决这个问题有点困难。这可能只是设置连接字符串或提供程序配置的方式的问题。
通常,您必须小心并行代码和EF。但是,你正在做什么 - 应该工作。我心中有一个问题;在并行之前,是否在该上下文的另一个实例上完成了任何工作?根据你的帖子,你在每个帖子中都有一个单独的上下文。这很好。然而,我的一部分想知道在多个上下文之间是否存在一些有趣的构造函数争用。如果你没有在该并行调用之前的任何地方使用该上下文,我建议尝试对上下文运行一个简单的查询来打开它,并确保在运行并行方法之前激活所有EF位。我承认,我还没有尝试完全你在这里做了什么,但我已经做得非常接近并且它已经奏效了。