IDのリストがあり、各IDでいくつかのストアドプロシージャを実行する必要があります。
標準のforeachループを使用している場合は問題なく動作しますが、レコードが多い場合は動作が非常に遅くなります。
EFで動作するようにコードを変換したかったのですが、「基になるプロバイダーがOpenで失敗しました」という例外が発生します。
Parallel.ForEach内でこのコードを使用しています:
using (XmlEntities osContext = new XmlEntities())
{
//The code
}
しかし、それでも例外はスローされます。
EFでParallelをどのように使用できますか?実行しているすべてのプロシージャに対して新しいコンテキストを作成する必要がありますか?私は約10のプロシージャを持っているので、それぞれに1つずつ、10のコンテキストを作成するのは非常に悪いことだと思います。
Entity Frameworkが使用している基本的なデータベース接続はnotthread-safe です。あなたは実行する別のスレッドで各操作の新しいコンテキストを作成する必要があります。
操作を並列化する方法に関するあなたの懸念は有効なものです。多くのコンテキストは開閉に費用がかかることになります。
代わりに、コードの並列化に関する考え方を逆にしたい場合があります。いくつかのアイテムをループしてから、各アイテムのストアドプロシージャをシリアルに呼び出しているようです。
可能であれば、それぞれに新しい Task<TResult>
(または結果が必要ない場合は Task
)を作成しますprocedure次に、そのTask<TResult>
で、単一のコンテキストを開き、すべてのアイテムをループしてから、ストアドプロシージャを実行します。このようにして、並行して実行しているストアード・プロシージャーの数と同じ数のコンテキストのみを使用できます。
2つのストアドプロシージャDoSomething1
とDoSomething2
のあるMyDbContext
があり、どちらもクラスのインスタンスMyItem
をとるとします。
上記を実装すると、次のようになります。
// 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.
できないストアドプロシージャを並列で実行する場合(それぞれが特定の順序で実行されることに依存している場合)、操作を並列化できます。 、それはもう少し複雑です。
あなたは、項目全体で creating custom partitions (static Create
methodPartitioner
class)を使用 =)。これは IEnumerator<T>
実装を取得する手段を提供します(注、これはnotIEnumerable<T>
なので、foreach
を超えることはできません)。
返されるIEnumerator<T>
インスタンスごとに、新しいTask<TResult>
を作成し(結果が必要な場合)、Task<TResult>
本文でコンテキストを作成してから循環しますIEnumerator<T>
によって返されたアイテム。ストアドプロシージャを順番に呼び出します。
これは次のようになります。
// Get the partitioner.
OrdinalPartitioner<MyItem> partitioner = Partitioner.Create(items);
// Get the partitions.
// You'll have to set the parameter for the number of partitions here.
// See the link for creating custom partitions for more
// creation strategies.
IList<IEnumerator<MyItem>> paritions = partitioner.GetPartitions(
Environment.ProcessorCount);
// Create a task for each partition.
Task[] tasks = partitions.Select(p => Task.Run(() => {
// Create the context.
using (var ctx = new MyDbContext())
// Remember, the IEnumerator<T> implementation
// might implement IDisposable.
using (p)
// While there are items in p.
while (p.MoveNext())
{
// Get the current item.
MyItem current = p.Current;
// Call the stored procedures. Process the item
ctx.DoSomething1(current);
ctx.DoSomething2(current);
}
})).
// ToArray is needed (or something to materialize the list) to
// avoid deferred execution.
ToArray();
これは私が使っているもので、うまくいきます。さらに、エラー例外の処理をサポートし、追跡モードをはるかに簡単にするデバッグモードを備えています。
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
内部例外の結果が何であるかを知らずにこれをトラブルシューティングするのは少し難しいです。これは、接続文字列またはプロバイダー構成のセットアップ方法に関する問題である可能性があります。
一般に、並列コードとEFには注意する必要があります。しかし、あなたがしていることは-うまくいくはずです-。私の頭の中の一つの質問。そのコンテキストの別のインスタンスbefore The parallelで行われている作業はありますか?あなたの投稿によると、あなたは各スレッドで別々のコンテキストを行っています。それは良い。ただし、複数のコンテキスト間で興味深いコンストラクターの競合が発生しているのではないかと思う人もいます。並列呼び出しの前にそのコンテキストを使用していない場合は、コンテキストに対して単純なクエリでも実行してコンテキストを開き、並列メソッドを実行する前にすべてのEFビットが起動されることを確認することをお勧めします。私は認めます、私は試していません正確にあなたがここでしたこと.