Reputation: 1048
I'm confused as to how ContinueWith works, it appears to block the ThreadPool and runs tasks sequentially. Take the following code example I have written:
var items = new List<int>();
for (int i = 0; i < 100; i++)
{
items.Add(i);
}
Parallel.ForEach(items, item =>
{
Task.Factory.StartNew(() =>
{
}).ContinueWith(t =>
{
if (item < 50)
{
System.Console.WriteLine("Blocking in {0}", item.ToString());
var x = SomeLongRunningDatabaseCall(10001);
}
System.Console.WriteLine("Item {0}", item);
});
});
The purpose of the code was to mirror a suspected thread blocking problem I had on a production application. To my surprise with the above code I found the issue seems to be with my use of ContinueWith. What's interesting is if I set the option TaskContinuationOptions.LongRunning on ContinueWith it runs the tasks asynchronously without blocking, I've also done this in my production app and this has fixed the issue.
However, I'm really confused and want to better understand why the ContinueWith statement without the option TaskContinuationOptions.LongRunning is causing the tasks to block or appear to run sequentially even though I'm invoking a new thread per iteration of items. The only thing I can think of is that ContinueWith is not running in the thread that the antecedent task was executed in but on the main thread which could be causing the block.
Any help or advice would be much appreciated.
Also what's interesting is that if I replace
SomeLongRunningDatabaseCall(10001)
with something like
Thread.Sleep(600000)
it doesn't block however with a call to the database it does but since it's running in it's own thread there shouldn't be any blockage for the other tasks at least, since tasks > 50 don't call the database.
Upvotes: 2
Views: 865
Reputation: 15794
Each thread should be using it's own database connection object. Most database connections that I'm aware of do not support non-locking multi-threaded modes. In those database connections, it's not unusual for the threads to be blocked, since the single connection will execute calls sequentially rather than in parallel.
That being the case, making a Parallel.ForEach() may just end up borking your app, since a database connection is (relatively) expensive, and it's not feasible, really, to open up a whole bunch of database connections. It might be better to use a different pattern, for instance putting the item
objects into a ConcurrentQueue
or a ConcurrentStack
and throwing 3-5 threads (or more - you'll have to experiment here) against this collection and letting these run until the stack or queue gets depleted. That way you can keep the number of instanced database connections to a safe number and still avoid that nasty blocking.
All of this leads me to think that using Parallel.ForEach
for actual database execution (not talking about LINQ-to-SQL or EF here, but actual database connection calls, like SqlCommand.Execute
) is probably sub-optimal.
EDIT: you don't have to use the old syntax for my suggestion. A clump of Tasks would do just as nicely.
Upvotes: 1