25

Task.Run(()=>{}) puts the action delegate into the queue and returns the task . Is there any benefit of having async/await within the Task.Run()? I understand that Task.Run() is required since if we want to use await directly, then the calling method will need to be made Async and will affect the calling places.

Here is the sample code which has async await within Task.Run(). The full sample is provided here: Create pre-computed tasks.

Task.Run(async () => { await new WebClient().DownloadStringTaskAsync("");});

Alternatively this could have been done:

Task.Run(() => new WebClient().DownloadStringTaskAsync("").Result;);

Since both, Task.Run() and Await will queue the work and will be picked by the thread pool, could the async/await within the Task.Run() be a bit redundant?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Chandan
  • 1,486
  • 2
  • 15
  • 24
  • This question is better answered in another post [Differences with and without async-await](https://stackoverflow.com/questions/61987924/task-run-with-or-without-async-and-await) – user2616989 Jul 26 '23 at 21:50

5 Answers5

20

Is there any benefit of having async/await within the Task.Run() ?

Yes. Task.Run runs some action on a thread-pool thread. If such action does some IO work and asynchronously waits for the IO operation to complete via await, then this thread-pool thread can be used by the system for other work while the IO operation is still running.

Example:

Task.Run( async () =>
{
    DoSomeCPUIntensiveWork();

    // While asynchronously waiting for this to complete, 
    // the thread is given back to the thread-pool
    var io_result = await DoSomeIOOperation(); 

    DoSomeOtherCPUIntensiveWork(io_result);
});
Filip Ekberg
  • 36,033
  • 20
  • 126
  • 183
Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
10

Is there any benefit of having async/await within the Task.Run()

An async method returns to the caller as soon as the first await is hit (that operates on a non-completed task). So if that first execution "streak" of an async method takes a long time Task.Run will alter behavior: It will cause the method to immediately return and execute that first "streak" on the thread-pool.

This is useful in UI scenarios because that way you can make 100% sure that you are not blocking the UI. Example: HttpWebRequestdoes DNS resolution synchronously even when you use one of the async methods (this is basically a library bug/design error). This can pause the UI thread. So you can use Task.Run to be 100% sure that the UI is never blocked for longer than a few microseconds.

So back to the original question: Why await inside a Task.Run body? For the same reason you normally await: To unblock the thread.

usr
  • 168,620
  • 35
  • 240
  • 369
  • "Why would you wrap an async/await code in Task.Run?!" Because for example I need to run async code in a synchornous method that either I can't change or is too risky to change all the callers.So then – David Jan 24 '22 at 14:29
  • 2
    @David In that case it usually suffices to call Wait/Result on the task that is being returned. – usr Jan 26 '22 at 07:34
2

In the example that you linked the main thread is being blocked until the asynchronous operation is done. It's being blocked by calling Wait() (which by the way is generally a bad idea).

Let's have a look at the return from the DownloadStringAsync in the linked sample:

return Task.Run(async () =>
{
    content = await new WebClient().DownloadStringTaskAsync(address);
    cachedDownloads.TryAdd(address, content);
    return content;
});

Why would you wrap this in a Task? Think about your options for a second. If you don't want to wrap this in a Task, how would you make sure the method returns a Task<string> and still have it work? You'd mark the method as async of course! However, if you mark your method as async and you call Wait on it, you'll most likely end up with a deadlock, since the main thread is waiting for the work to finish, and your blocking the main thread so it can't let you know it's done.

When marking a method as async, the state machine will run on the calling thread, in your example however, the state machine runs on a separate thread, meaning there is little to no work being done on the main thread.

Filip Ekberg
  • 36,033
  • 20
  • 126
  • 183
2

Calling Async from Non-Async Methods

We do some stuff like that when we are trying to call an async method inside of a non-async method. Especially if the async method is a known quantity. We use more of a TaskFactory though ... fits a pattern, makes it easier to debug, makes sure everyone takes the same approach (and -- gives us one throat to choke if async-->sync starts acting buggy).

So, Your Example

Imagine, in your example, that you have a non-async function. And, within that function, you need to call await webClient.DoSomethingAsync(). You can't call await inside of a function that's not async -- the compiler won't let you.

Option 1: Zombie Infestation

Your first option is to crawl all the way back up your call stack, marking every da*n method along the way as async and adding awaits everywhere. Everywhere. Like, everywhere. Then -- since all those methods are now async, you need to make the methods that reference them all async.

This, btw, is probably the approach many of the SO enthusiasts are going to advocate. Because, "blocking a thread is a bad idea."

So. Yeah. This "let async be async" approach means your little library routine to get a json object just reached out and touched 80% of the code. Who's going to call the CTO and let him know?

Option 2: Just Go with the One Zombie

OR, you can encapsulate your async inside of some function like yours...

return Task.Run(async () => {
     content = await new WebClient().DoSomethingAsync();
     cachedDownloads.TryAdd(address, content);
     return content;
});

Presto... the zombie infestation has been contained to a single section of code. I'll leave it to the bit-mechanics to argue over how/why that gets executed at the CPU-level. I don't really care. I care that nobody has to explain to the CTO why the entire library should now be 100% async (or something like that).

bri
  • 2,932
  • 16
  • 17
-3

Confirmed, wrapping await with Task.Run use 2 threads instead of one.

Task.Run(async () => { //thread #1
 await new WebClient().DownloadStringTaskAsync(""); //thread #2
});

Say you have four calls wrapped like this, it will use 4 x 2 = 8 threads.

It would be better to just call these with simple await instead. For example:

Task<byte[]> t1 = new WebClient().DownloadStringTaskAsync("");
Task<byte[]> t2 = new WebClient().DownloadStringTaskAsync("");
byte[] t1Result = await t1;
byte[] t2Result = await t2;

Here is the proof that wrapped Task.Run are using extra threads. (Not using WebClient to prove the point)

private static async Task wrapped()
{
    List<Task> tasks = new List<Task>();
    tasks.AddRange(new []
    {
        Task.Run(async() => await new MyThread().RunMe()),
        Task.Run(async() => await new MyThread().RunMe()),
        Task.Run(async() => await new MyThread().RunMe()),
        Task.Run(async() => await new MyThread().RunMe()),
    });

    Thread.Sleep(1000);
    int number = Process.GetCurrentProcess().Threads.Count;
    Console.WriteLine($"While running thread count: {number}");

    await Task.WhenAll(tasks);
}

enter image description here

Unwrapped

private static async Task unwrapped()
{
    List<Task> tasks = new List<Task>();
    Task<int> t1 = new MyThread().RunMe();
    Task<int> t2 = new MyThread().RunMe();
    Task<int> t3 = new MyThread().RunMe();
    Task<int> t4 = new MyThread().RunMe();
    tasks.AddRange(new[] {t1, t2, t3, t4});

    Thread.Sleep(1000);
    int number = Process.GetCurrentProcess().Threads.Count;
    Console.WriteLine($"While running thread count: {number}");

    int i1 = await t1;
    int i2 = await t2;
    int i3 = await t3;
    int i4 = await t4;
}

enter image description here

Full POC code here

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncThreadDemo
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"Init thread count: {number}");


            //await wrapped();
            await unwrapped();

            number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"Done thread count: {number}");

            Console.ReadLine();
        }

        private static async Task wrapped()
        {
            List<Task> tasks = new List<Task>();
            tasks.AddRange(new []
            {
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
            });

            Thread.Sleep(1000);
            int number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"While running thread count: {number}");

            await Task.WhenAll(tasks);
        }

        private static async Task unwrapped()
        {
            List<Task> tasks = new List<Task>();
            Task<int> t1 = new MyThread().RunMe();
            Task<int> t2 = new MyThread().RunMe();
            Task<int> t3 = new MyThread().RunMe();
            Task<int> t4 = new MyThread().RunMe();
            tasks.AddRange(new[] {t1, t2, t3, t4});

            Thread.Sleep(1000);
            int number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"While running thread count: {number}");

            int i1 = await t1;
            int i2 = await t2;
            int i3 = await t3;
            int i4 = await t4;
        }
    }

    public class MyThread
    {
        public static int _counter;

        public async Task<int> RunMe()
        {
            await Task.Run(() =>
            {
                for (int i = 0; i < 2; ++i)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"T{Thread.CurrentThread.ManagedThreadId} {i}");
                }
                Console.WriteLine($"T{Thread.CurrentThread.ManagedThreadId} done");
            });
            return _counter++;
        }
    }
}
Jeson Martajaya
  • 6,996
  • 7
  • 54
  • 56
  • Jeson yeap, the answer is now OK regarding the StackOverflow rules, but in its essense it's not a great answer. The `Task.Run` method does not spawn new threads, it reuses `ThreadPool` threads. And the `WebClient.DownloadStringTaskAsync` for the most part uses [zero threads](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). The `ThreadPool` is utilized for so brief durations, that a single thread may be able to do all the work needed. – Theodor Zoulias Jul 30 '21 at 06:41
  • @TheodorZoulias word `spawn` renamed to `use`. Added POC as well. Hopefully this proves the point. Wrapping async call in a `Task.Run` causes unnecessary overhead. – Jeson Martajaya Aug 02 '21 at 14:47
  • I can't reproduce the results of your Proof of Concept program. In my computer it's printed `Done thread count: 14` with either the `await wrapped();` or the `await unwrapped();`. And I get a similar output [on Fiddle](https://dotnetfiddle.net/SJFnPQ). I added logging of the `ThreadPool.ThreadCount` for completeness. Btw your PoC is not analogous to your original `WebClient` example. Instead of calling a genuinely asynchronous operation, it blocks a thread with `Thread.Sleep(1000);`. Use `await Task.Delay(1000);` to keep the PoC relevant ([try it on Fiddle](https://dotnetfiddle.net/AA1DCh)). – Theodor Zoulias Aug 02 '21 at 19:17
  • Looks like this issue is .NET version specific. I have the PoC in .NET framework 4.7.2, VS2019, windows machine. Ran without problem. .NET Fiddle however, in .NET framework 4.7.2, it throws Site Security error when calling Proess.GetCurrentProcess. Try running the PoC code under 4.7.2 in your local machine. https://dotnetfiddle.net/plS0ka Also to be fair, in MyThread class, use Thread.Sleep(1000) instead of await Task.Delay(1000). The latter is more efficient as it releases thread while waiting. The former is holding it. – Jeson Martajaya Aug 02 '21 at 20:40
  • Jeson what your PoC is observing is the behavior of the `ThreadPool` class, which has probably changed from .NET Framework to .NET Core. Its behavior is also dependent on the number of CPU cores, and I guess that your machine has more than 4. Just comparing the number of threads at various moments says nothing about the total processing power that a program has consumed so far. And insisting at simulating the `WebClient.DownloadStringTaskAsync` with `Task.Run(() => Thread.Sleep())` makes your answer irrelevant to the question. The former method doesn't put threads to sleep. – Theodor Zoulias Aug 03 '21 at 00:42
  • @TheodorZoulias, did you try running my PoC as-is in VS2019, .NET Framework 4.7.2 ? That should settle this debate whether wrapping in async is harmful. Best case it has minimal benefits, and worst case it uses an extra thread per call, as exhibited by the POC. No need to argue with words anymore. – Jeson Martajaya Aug 03 '21 at 13:56