0

If I have this

static void Main(string[] args)
{
    var tasks = new List<Task>();
    for (int i = 0; i < 10; i++)
    {
        tasks.Add(AsyncWithBlockingIO(i));
    }

    Task.WaitAll(tasks.ToArray());
}

private static async Task AsyncWithBlockingIO(int fileNum)
{
    var result = await File.ReadAllTextAsync($"File{fileNum}").ConfigureAwait(false);
    //will the below run concurrently on multiple threads?
    CpuNoIOWork(result);
}

Will CpuNoIOWork() run concurrently on multiple threads (using the thread pool) as the IO calls complete or will it only use 1 thread at a time?

axk
  • 5,316
  • 12
  • 58
  • 96
  • 1
    You can add `Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);` to `CpuNoIOWork()` to prove that the continuations are indeed all run concurrently. – Matthew Watson Apr 15 '21 at 13:54
  • 1
    You may want to take a look at this: [Why File.ReadAllLinesAsync() blocks the UI thread?](https://stackoverflow.com/questions/63217657/why-file-readalllinesasync-blocks-the-ui-thread) Not all Async-advertised methods are genuinely asynchronous. – Theodor Zoulias Apr 15 '21 at 16:47
  • 1
    @TheodorZoulias, thanks! not using the file APIs in the real code, but useful to know. – axk Apr 15 '21 at 17:43

1 Answers1

7

Will CpuNoIOWork() run concurrently on multiple threads (using the thread pool) as the IO calls complete or will it only use 1 thread at a time?

It will run concurrently, on thread pool threads.

However, depending on the internals of what you're awaiting, it might continue on an I/O thread pool thread, which is not desirable. If you have CPU-bound work, I recommend wrapping that in Task.Run, which will ensure the work will be run on a worker thread pool thread:

var result = await File.ReadAllTextAsync($"File{fileNum}").ConfigureAwait(false);
await Task.Run(() => CpuNoIOWork(result));
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Instead of `await Task.Run` in this specific case, would `await Task.Yield()` force the continuation to run on a threadpool thread? – JohanP Apr 15 '21 at 23:24
  • @JohanP: Yes, but I prefer `Task.Run` so that the code is more explicit. – Stephen Cleary Apr 16 '21 at 00:14
  • @JohanP the `Task.Yield` is a poor man's substitute of the non-existing API [`TaskScheduler.SwitchTo`](https://stackoverflow.com/questions/15363413/why-was-switchto-removed-from-async-ctp-release). The behavior of the `Task.Yield` depends on the ambient `SynchronizationContext.Current` (or the ambient `TaskScheduler.Current` if the context is null), so it's not a reliable (platform agnostic) way to switch to the `ThreadPool`, if that's your intention. – Theodor Zoulias Apr 16 '21 at 06:58
  • @TheodorZoulias Would you say in .net core that it is a reliable way to get to threadpool thread in this specific instance? – JohanP Apr 16 '21 at 07:50
  • @JohanP no, I wouldn't. It's not the right tool for the job IMHO. If I really wanted to avoid the `Task.Run` for some reason, I would use a custom awaiter like noseratio's [`TaskSchedulerAwaiter`](https://gist.github.com/noseratio/5d2d5f2a0cbb71b7880ce731c3958e62#file-taskschedulerawaiter-cs-L33), or a simplified version of it. – Theodor Zoulias Apr 16 '21 at 08:59