14

I've tried to read up on async methods and am now trying to create my own async method. The method is a webservice call that returns a list of error logs. I'm not sure that I've understood correctly so I thought I'd share my code to see if I should do anything different.

All I want the code to do is return a list of errorlogs by calling a method GetAllErrorLogs(), that is a synchronized method. Since it can take a second to fetch all the error logs I want to have the opportunity to do other stuff once I called the GetAllErrorLogs() method. Here is the code.

[WebMethod]
public async Task<List<ErrorLog>> GetAllErrorLogs()
{
    List<ErrorLog> errorLogs = new List<ErrorLog>();

    await System.Threading.Tasks.Task.Run(() => {
        errorLogs = ErrorLogRepository.GetAllErrorLogs();
    });


    if (errorLogs == null)
        return new List<ErrorLog>();

    return errorLogs;
}

Thanks!

Andreas
  • 2,336
  • 2
  • 28
  • 45
  • 1
    I don't see much advantage using async/await on the server side. You just use more threads to the same thing. – I4V Aug 21 '13 at 20:38
  • 6
    @I4V: `async` on the server side can dramatically *reduce* the number of threads used per request (assuming the code is naturally asynchronous and not fake-asynchronous such as `Task.Run`). As a result, asynchronous servers are able to scale much better, often on the order of 10-100x. – Stephen Cleary Aug 21 '13 at 21:01
  • 2
    Stephan Cleary is correct... I attended a training course at a microsoft campus where we were told that just by asyncing all the code run on your server, you would get massive increases in capacity for the same hardware. Purely because, when a method is waiting for a response from a sub call, the main thread is released to go and do other work... ie deal with other concurrent web requests. The advantage is multitasking. – GPR May 17 '16 at 15:25
  • [realted post](https://stackoverflow.com/questions/15522900/how-to-safely-call-an-async-method-in-c-sharp-without-await) – OfirD May 14 '19 at 10:46

5 Answers5

9

I recently gave a talk at ThatConference on async on the server side, and I address this issue in the slides.

On the server side, you want to avoid the use of Task.Run and other constructs that queue work to the thread pool. As much as possible, keep thread pool threads available for handling requests.

So, ideally your repository would have an asynchronous method GetAllErrorLogsAsync, which would itself be asynchronous. If GetAllErrorLogs cannot be asynchronous, then you may as well just call it directly (removing the await Task.Run).

Since it can take a second to fetch all the error logs I want to have the opportunity to do other stuff once I called the GetAllErrorLogs() method.

If you have a GetAllErrorLogsAsync available, then this can easily be done using Task.WhenAll. However, if GetAllErrorLogs is synchronous, then you can only do this by doing parallel work in your request (e.g., multiple calls to Task.Run followed by Task.WhenAll).

Parallel code on the server must be approached with great trepidation. It is only acceptable in a very limited set of scenarios. The entire point of async on the server side is to use fewer threads per request, and when you start parallelizing, you're doing the opposite: multiple threads per request. This is only appropriate if you know your user base is very small; otherwise, you'll kill your server scalability.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    The complexities of parallelism are too long for here and off-topic, but it should be noted the warning against parallelism here is overbroad. If you're awaiting a Web Request to a webservice and a database call, you're leveraging I/O Completion Port threads that are super cheap, OS-level, and don't eat threads from the ThreadPool. A perfect scenario for parallelism that speeds things up for the user without blitzing the Request Threadpool. The right answer here would be "Here's how" not "Don't do this." http://stackoverflow.com/a/539968/176877 – Chris Moschini Jan 27 '15 at 22:45
  • 1
    @ChrisMoschini: I make a distinction between *multithreading/parallelism* (`Task.Run`, `Parallel`, etc) and *asynchrony* (`async`, `await`, etc), both of which are [different forms of *concurrency*](https://pbs.twimg.com/media/B63AADfIgAA4jPH.jpg:large). With those definitions, parallelism is bad on the server side. – Stephen Cleary Jan 28 '15 at 01:24
  • @ChrisMoschini: Feel free to post your own answer as well as downvoting mine. :) – Stephen Cleary Jan 28 '15 at 12:57
2

I found this great codeproject detailed article about how to achieve that

http://www.codeproject.com/Articles/600926/Asynchronous-web-services-call-in-ASP-NET

Mauricio Gracia Gutierrez
  • 10,288
  • 6
  • 68
  • 99
1

**This is potentially wrong, read comments or spinoff question at HttpContext.Current after an await

If ErrorLogRepository.GetAllErrorLogs() is not thread-safe, it will cause weird bugs and potentially exception out. Make sure your code is ready for multi-threaded operation before switching to async methods, this is obviously very trivial advice but often overlooked. For example, if you reference HttpContext.Current in your methods, your code will die in the async method, and sometimes even AFTER the await. The reason is that the code within the async block will potentially be run on a separate thread, which will not have access to the same HttpContext.Current thread-static property, and await gets compiled into two methods. All code before an await gets run on one thread, and then calls the code after an await keyword as a continuation, but potentially on yet another thread. So sometimes your code will even work in an async block, only to choke unexpectedly after it gets "out" of the async back to what you think is a synchronous part of your code (but in reality everything after an await keyword is already not guaranteed to be the original thread).

Community
  • 1
  • 1
welegan
  • 3,013
  • 3
  • 15
  • 20
  • There's a lot of misinformation in this answer. `UnobservedTaskException` is not required here (`await` will properly propagate any exceptions from `GetAllErrorLogs` up through the WebAPI method). `HttpContext.Current` is properly propagated to all threads that handle an async request, by default (i.e., the `return errorLogs` line has a perfectly valid `HttpContext.Current`). – Stephen Cleary Aug 21 '13 at 20:59
  • You're right about await, I misread that part. It would only not be propogated in the event of Task.Run being used as a fire and forget. However, HttpContext.Current will definitely not be valid after the await unless WebAPI has overridden the default synchronization context behavior. – welegan Aug 21 '13 at 21:17
  • 1
    WebAPI does not provide a synchronization context, but ASP.NET does. So `HttpContext.Current` is perfectly valid after the `await`. – Stephen Cleary Aug 21 '13 at 22:28
  • @StephenCleary, *WebAPI does not provide a synchronization context* - hmm, could there be any cases when it's still `AspNetSynchronizationContext` for Web API? Here is a [related WebAPI question](http://stackoverflow.com/q/18284998/1768303) where I asked the OP to `Debug.Print(SynchronizationContext.Current.GetType().Name)` and it was `AspNetSynchronizationContext`. Until now I was under impression this the default behavior for Web API. – noseratio Aug 22 '13 at 05:08
  • 1
    @Noseratio: That is the default behavior. That's the synchronization context provided by ASP.NET, which WebAPI uses. – Stephen Cleary Aug 22 '13 at 12:16
  • @StephenCleary, thanks for clarifying. I misread your comment above: got it like WebAPI does not provide *any* s.context, while ASP.NET does. Outside that, my question doesn't make sense :] – noseratio Aug 22 '13 at 12:35
  • I edited answer to reflect your reply. I'm not sure why one project I had would not resolve .Current but it must have been something I did that modified default behavior because starting a default web project from start in VS2012 ended up resolving `HttpContext.Current` after an await just fine. – welegan Aug 22 '13 at 14:33
  • Spoke too soon, I wrote a new test and it showed me different results. Could you answer: http://stackoverflow.com/questions/18383923/why-is-httpcontext-current-null @StephenCleary ? – welegan Aug 22 '13 at 14:55
0

Here is some production code...

using System.Web.Http;
using AysncTask = System.Threading.Tasks.Task;

public class myController : ApiControllerBase
{
        [HttpPut]
        [Route("api/cleardata/{id}/{requestId}/")]
        public async AysncTask ClearData(Guid id, Guid requestId)
        {
            try
            {
                await AysncTask.Run(() => DoClearData(id, requestId));
            }
            catch (Exception ex)
            {
                throw new Exception("Exception in myController.ClearData", ex);
            }
        }
}
hamish
  • 1,141
  • 1
  • 12
  • 21
0

Handling Async exceptions is also VERY VERY important.. although this is for a windows console app, the same principles should apply.

source: https://blogs.msdn.microsoft.com/ptorr/2014/12/10/async-exceptions-in-c/

  using System;
  using System.Runtime.CompilerServices;
  using System.Threading;
  using System.Threading.Tasks;

  namespace AsyncAndExceptions
  {
class Program
{
  static void Main(string[] args)
  {
    AppDomain.CurrentDomain.UnhandledException += (s, e) => Log("*** Crash! ***", "UnhandledException");
    TaskScheduler.UnobservedTaskException += (s, e) => Log("*** Crash! ***", "UnobservedTaskException");

    RunTests();

    // Let async tasks complete...
    Thread.Sleep(500);
    GC.Collect(3, GCCollectionMode.Forced, true);
  }

  private static async Task RunTests()
  {
    try
    {
      // crash
      // _1_VoidNoWait();

      // crash 
      // _2_AsyncVoidAwait();

      // OK
      // _3_AsyncVoidAwaitWithTry();

      // crash - no await
      // _4_TaskNoWait();

      // crash - no await
      // _5_TaskAwait();

      // OK
      // await _4_TaskNoWait();

      // OK
      // await _5_TaskAwait();
    }
    catch (Exception ex) { Log("Exception handled OK"); }

    // crash - no try
    // await _4_TaskNoWait();

    // crash - no try
    // await _5_TaskAwait();
  }

  // Unsafe
  static void _1_VoidNoWait()
  {
    ThrowAsync();
  }

  // Unsafe
  static async void _2_AsyncVoidAwait()
  {
    await ThrowAsync();
  }

  // Safe
  static async void _3_AsyncVoidAwaitWithTry()
  {
    try { await ThrowAsync(); }
    catch (Exception ex) { Log("Exception handled OK"); }
  }

  // Safe only if caller uses await (or Result) inside a try
  static Task _4_TaskNoWait()
  {
    return ThrowAsync();
  }

  // Safe only if caller uses await (or Result) inside a try
  static async Task _5_TaskAwait()
  {
    await ThrowAsync();
  }

  // Helper that sets an exception asnychronously
  static Task ThrowAsync()
  {
    TaskCompletionSource tcs = new TaskCompletionSource();
    ThreadPool.QueueUserWorkItem(_ => tcs.SetException(new Exception("ThrowAsync")));
    return tcs.Task;
  }
  internal static void Log(string message, [CallerMemberName] string caller = "")
  {
    Console.WriteLine("{0}: {1}", caller, message);
  }
}

}

hamish
  • 1,141
  • 1
  • 12
  • 21