0

I needed a way to shave some additional time off of some reporting queries and opted to use async methods to perform some of the tasks in parallel. I'm pretty new to async so I made a test console application to prove the concept before plugging it into my asp.net MVC application. Everything works as expected in the console app but the same code never gets past "whenall" within asp.net. Here is my code:

//Synchronous call for outside world
public DataSet GetQueries_Sync()
{
    Task<DataSet> Out = GetQueries();
    Out.Wait();
    return Out.Result;
}

//Run all needed tasks in parallel
private async Task<DataSet> GetQueries()
{
    Task<DataTable> Task1 = QueryOne();
    Task<DataTable> Task2 = QueryTwo();
    Task<DataTable>[] Tasks = new Task<DataTable>[] { Task1, Task2 };
    await Task.WhenAll(Tasks);
    DataSet Out = new DataSet();
    Out.Tables.Add(Task1.Result);
    Out.Tables.Add(Task2.Result);
    return Out;
}

//Individual Queries
private string ConnString = "MyConnectionString";
private Task<DataTable> QueryOne()
{
    return Task.Run(() =>
    {
        DataTable Out = new DataTable();
        string SQL = "";
        SqlConnection Conn = new SqlConnection(ConnString);
        SqlDataAdapter Adapter = new SqlDataAdapter(SQL, Conn);
        Conn.Open();
        Adapter.Fill(Out);
        Out.TableName = "QueryOne";
        Conn.Close();
        return Out;
    });
}

private Task<DataTable> QueryTwo()
{
    return Task.Run(() =>
    {
        DataTable Out = new DataTable();
        string SQL = "SQL Statement #2, ~30sec";
        SqlConnection Conn = new SqlConnection(ConnString);
        SqlDataAdapter Adapter = new SqlDataAdapter(SQL, Conn);
        Conn.Open();
        Adapter.Fill(Out);
        Out.TableName = "QueryTwo";
        Conn.Close();
        return Out;
    });
}

In asp.net (.net Framework 4.7), nothing past "Task.WhenAll(Tasks)" within GetQueries() is run even though each of the individual query functions return their results. The console app is basically the same except that the methods are static. Task.WhenAll(Tasks) continues as expected in the console environment. Any ideas why the same code would work within a console app but not within an asp.net app?

magnvs78
  • 13
  • 2
  • 3
    You used `.Wait()` and `.Result`. These are code smells. You need to await the results and have the method marked as async and return a Task (or Task) all the way up the call stack as far as you can. See [Async Await Best Practices](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming). You need to change `GetQueries_Sync` and how it's called. – mason Dec 11 '19 at 18:14
  • So, you show working code and asking about not working? – Selvin Dec 11 '19 at 18:16
  • Your SqlConnection handling is not good. You need to follow the proper [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable?view=netframework-4.8) patterns. – mason Dec 11 '19 at 18:17
  • @Selvin: the code is the same just different environments. this code works in a console app but not in an asp.net app – magnvs78 Dec 11 '19 at 18:19
  • Are you getting any exceptions? Add exception handlers to make sure code is not getting an exception due to running two SQL connections in parallel. – jdweng Dec 11 '19 at 18:19
  • @mason no debate there... i'm trying to get the concept to work and then i'll worry about best practices – magnvs78 Dec 11 '19 at 18:20
  • @jdweng i don't think so as i'd expect the same behavior in both environments... i'll add the handlers to be 100% sure and will report back. stuck in a meeting now but will do it as soon as possible – magnvs78 Dec 11 '19 at 18:24
  • The `Out.Wait()` call simply blocks execution of `await Task.WhenAll(Tasks)` continuation under ASP.NET because it uses `SynchronizationContext` unlike a console application. [Don't block on async code](https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html). – Dmytro Mukalov Dec 11 '19 at 18:28

2 Answers2

0

The reason why this happens in ASP.Net and not in the console app, it's due to how the former works behind the scenes when switching from an asynchronous context to a synchrounous context... to put it shortly, when you try to do something like this, you get a deadlock.

There are some ways to alleviate this behaviour (ConfigureAwait) but when you go on this path you have to be careful with how you chain/manage threads etc... that's why it's not usually recommended.

You can read more about this here: Fun with ConfigureAwait, and deadlocks in ASP.NET

Note that this doesn't happen with ASP.Net Core.

Dan
  • 1,555
  • 2
  • 14
  • 30
0

There are two ways to wait for an asynchronous task to finish.

Blocking. The following will block the current thread and wait for the task to finish.

task.Wait();
task.Result;
task.GetAwaiter().GetResult();

Nonblocking. The following breaks the method in two and posts the remainder of it, called the "continuation," to the synchronization context. It then returns control to the caller, along with a task that represents the continuation. The caller can force the continuation to complete by awaiting the task, using another await, recursively.

await task;

To a developer, the flow of control looks very similar, but they are in fact very different in terms of what thread run what piece of code. The difference depends on the specific SynchronizationContext, which varies by application type. ASP.NET, for example, uses AspNetSynchronizationContext, while a WinForms app runs with a WindowsFormsSynchronizationContext.

In a console application, the synchronization context is just the thread pool, and is free-threaded. So you can use either technique without deadlock. The continuation will be able to execute even if the current thread is blocked, so Wait(), Result, and GetResult() will eventually return.

In an ASP.NET application, the synchronization context is designed to allow for execution on a single thread (not exactly, due to to thread agility, but you can read about that separately). This allows you to write ASP.NET code without worrying about locking or race conditions (mostly). But it also means that blocking the current thread will prevent the continuation from running. Since the current thread is blocking until the continuation has finished, and the continuation can't be executed, you have a deadlock situation. Wait(), Result, and GetResult() will never return, and the application will hang.

You can avoid the problem by ensuring that you only wait for tasks to complete using await. However this means that the method must be marked async.

If you literally can't mark the method async, you have to use a workaround, which is more complicated than you might think. See Stephen Cleary's excellent answer to learn how.

See also this answer on async flow of control and this answer which provides a simple deadlock example.

John Wu
  • 50,556
  • 8
  • 44
  • 80