If you need to replicate your original Thread
-based behaviour, you can use Task.Factory.StartNew(... , TaskCreationOptions.LongRunning)
to schedule your work, and then block until the worker tasks complete via Task.WaitAll
. I do not recommended this approach, but in terms of behaviour this will be very close to how your code was working previously.
A more in-depth analysis as to why may not getting the expected performance in your scenario is as follows:
Explanation, part 1 (async
does not mean "on a different thread")
Methods marked with the async
keyword do not magically run asynchronously. They are merely capable of combining awaitable operations (that may or may not run asynchronously themselves), into a single larger unit (generally Task
or Task<T>
).
If your insert
method is async
, it is still likely that it performs at least some of the work synchronously. This will definitely be the case with all of your code preceding the first await
statement. This work will execute on the "main" thread (thread which calls insert
) - and that will be your bottleneck or at least part thereof as the degree of parallelism for that section of your code will be 1 while you're calling insert
in a tight loop, regardless of whether you await
the resulting task.
To illustrate the above point, consider the following example:
void Test()
{
Debug.Print($"Kicking off async chain (thread {Thread.CurrentThread.ManagedThreadId}) - this is the main thread");
OuterTask().Wait(); // Do not block on Tasks - educational purposes only.
}
async Task OuterTask()
{
Debug.Print($"OuterTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
await InnerTask().ConfigureAwait(false);
Debug.Print($"OuterTask after await (thread {Thread.CurrentThread.ManagedThreadId})");
}
async Task InnerTask()
{
Debug.Print($"InnerTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
await Task.Delay(10).ConfigureAwait(false);
Debug.Print($"InnerTask after await (thread {Thread.CurrentThread.ManagedThreadId}) - we are now on the thread pool");
}
This produces the following output:
Kicking off async chain (thread 6) - this is the main thread
OuterTask before await (thread 6)
InnerTask before await (thread 6)
InnerTask after await (thread 8) - we are now on the thread pool
OuterTask after await (thread 8)
Note that the code before the first await
inside Task1
and even Task2
still executes on the "main" thread. Our chain actually executes synchronously, on the same thread which kicked off the outer task, until we await
the first truly async operation (in this case Task.Delay
).
Additionally
If you are running in an environment where SynchronizationContext.Current
is not null (i.e. Windows Forms, WPF) and you're not using ConfigureAwait(false)
on the tasks awaited inside your insert
method, then continuations scheduled by the async state machine after the first await
statement will also likely execute on the "main" thread - although this is not guaranteed in certain environments (i.e. ASP.NET).
Explanation, part 2 (executing Task
s on the thread pool)
If, as part of your insert
method, you are opting to start any Task
s manually, then you are most likely scheduling your work on the thread pool by using Task.Run
or any other method of starting a new task that does not specify TaskCreationOptions.LongRunning
. Once the thread pool gets saturated any newly started tasks will be queued, thus reducing the throughput of your parallel system.
Proof:
IEnumerable<Task> tasks = Enumerable
.Range(0, 200)
.Select(_ => Task.Run(() => Thread.Sleep(100))); // Using Thread.Sleep to simulate blocking calls.
await Task.WhenAll(tasks); // Completes in 2+ seconds.
Now with TaskCreationOptions.LongRunning
:
IEnumerable<Task> tasks = Enumerable
.Range(0, 200)
.Select(_ => Task.Factory.StartNew(
() => Thread.Sleep(100), TaskCreationOptions.LongRunning
));
await Task.WhenAll(tasks); // Completes in under 130 milliseconds.
It is generally not a good idea to spawn 200 threads (this will not scale well), but if massive parallelisation of blocking calls is an absolute requirement, the above snippet shows you one way to do it with TPL.