Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the execution order/logic : Task.Run( async() => await someAsyncMethod(); )

When reading How to: Create Pre-Computed Tasks the example method DownloadStringAsync returns

Task.Run( async ()=> { return await new WebClient().DownloadStringTaskAsync(address)}) 

I wonder why we need to wrap a async method in a Task.Run()? the WebClient().DownloadStringTaskAsync() method returns a Task itself.

like image 285
zheng yu Avatar asked Mar 06 '23 17:03

zheng yu


1 Answers

I think the problem is that they wanted to show how to use Task.FromResult and then tied themselves into knots because they wanted to consume a Task returning method.

The natural way these days to write code that consumes Task returning methods is to make them async. But then, if you do that, Task.FromResult disappears:

   // Asynchronously downloads the requested resource as a string.
   public static async Task<string> DownloadStringAsync(string address)
   {
      // First try to retrieve the content from cache.
      string content;
      if (cachedDownloads.TryGetValue(address, out content))
      {
         return content;
      }

      content = await new WebClient().DownloadStringTaskAsync(address);
      cachedDownloads.TryAdd(address, content);
      return content;
   }

Simpler code, still achieves the overall goal. Unless you expect cachedDownloads.TryAdd to be significantly CPU-heavy in which case their version also guarantees to push that into running in the thread pool.

In short - don't copy this code, it's not a good example to work from2.


This is the version that avoids allocating the async state machine1 when not required, shows Task.FromResult and still doesn't use Task.Run:

   // Asynchronously downloads the requested resource as a string.
   public static Task<string> DownloadStringAsync(string address)
   {
      // First try to retrieve the content from cache.
      string content;
      if (cachedDownloads.TryGetValue(address, out content))
      {
         return Task.FromResult(content);
      }

      return DownloadStringSlowAsync(address);
   }
   private static async Task<string> DownloadStringSlowAsync(string address)
   {
      string content = await new WebClient().DownloadStringTaskAsync(address);
      cachedDownloads.TryAdd(address, content);
      return content;
   }

Even betterer: (no, it's not a word, I don't care)

   static ConcurrentDictionary<string, Task<string>> cachedDownloads =
   new ConcurrentDictionary<string, Task<string>>();
   // Asynchronously downloads the requested resource as a string.
   public static Task<string> DownloadStringAsync(string address)
   {
      // First try to retrieve the content from cache.
      Task<string> content;
      if (cachedDownloads.TryGetValue(address, out content))
      {
         return content;
      }

      return DownloadStringSlowAsync(address);
   }
   private static async Task<string> DownloadStringSlowAsync(string address)
   {
      string content = await new WebClient().DownloadStringTaskAsync(address);
      cachedDownloads.TryAdd(address, Task.FromResult(content));
      return content;
   }

Because now our cache only contains completed tasks and we can just hand them out over and over rather than repeatedly allocating new Task objects on every request.

Any of these approaches is, of course, only really viable if the cached objects (string here) are immutable.


1Don't do this automatically. It should be a deliberate, considered decision based upon whether such allocations are causing performance issues.

2It's also a bad example of a cache, since as Raymond Chen points out, A cache with a bad policy is another name for a memory leak. In this example there's no expiration at all.

like image 122
Damien_The_Unbeliever Avatar answered Mar 09 '23 05:03

Damien_The_Unbeliever