Tuesday 16 February 2016

Async/Await - avoid misuse

Since the introduction of async/await constructs in C#/VB languages for .NET runtime, it has become more and more popular. I know a lot of folks who have switched to using async/await instead of any of the old asynchronous programming patterns. 


However, there are few common pitfalls. E.g.

1. Developers get carried away with the usage of await keyword and ConfigureAwait function and therefore they end up using it in places where it is actually not required. For example -

private async Task GetValueFromStore(string key)
{
            string val = this.cache[key];
            if(val != null)
            {
                return await Task.FromResult(val).ConfigureAwait(false);
            }
            else
            {
                val = await GetFromDBAsync(key).ConfigureAwait(false);
                //insert into cache
                return await Task.FromResult(val).ConfigureAwait(false);
            }
}



In the above example, Task.FromResult are unnecessary. It could have been simply:

private async Task GetValueFromStore(string key)
{
            string val = this.cache[key];
            if(val != null)
            {
                return val;
            }
            else
            {
                val = await GetFromDBAsync(key).ConfigureAwait(false);
                //insert into cache
                return val;
            }
}



Updated method saves unnecessary creation of Task objects on heap (btw, Task.FromResult sets the status to RanToCompletion at the time of creation and is not queued on thread pool). Check discussion about Task.FromResult.


2. Sometimes folks use the elegant chain of asyn/await in the control flow till they hit a roadblock and simply change the last block to use Result property of Task to make it run immediately (and on calling thread) in blocking manner. That is bad - it gives false hope that flow will be run using async/await constructs.

One such scenario is to run multiple SQL queries inside a transaction. Till .NET 4.5 there was no support for async tasks inside TransactionScope and therefore developers used code blocks like following:

[HttpGet]
public async Task DbQuery()
{
            using (TransactionScope s = new TransactionScope())
            {
                var result = await ExecuteStoredProcedure("usp_Procedure1", null).ConfigureAwait(false);
                //result = -1;
                var result2 = await ExecuteStoredProcedure("usp_Procedure2", result).ConfigureAwait(false);
                s.Complete();
            }
            return new ContentResult() { Content = "SP run done", ContentType = "text/html" };
}
          
private async Task ExecuteStoredProcedure(string spName, int? val)
{
            using (SqlConnection conn = new SqlConnection("Server=.;Initial Catalog=Test;Integrated Security=SSPI;"))
            {
                SqlCommand command = new SqlCommand();
                command.Connection = conn;
                command.CommandText = spName;
                command.CommandType = System.Data.CommandType.StoredProcedure;
                if (val.HasValue)
                {
                    command.Parameters.Add(new SqlParameter("Table1Id", val.Value));
                }
                conn.Open();
                using (var reader = command.ExecuteReaderAsync().Result)
                {
                    if (reader.Read())
                    {
                        return Convert.ToInt32(reader["Identity"]);
                    }
                }
            }
            return -1;
}

This issue was fixed in .NET 4.5.1. So, instead of getting stuck with above code, you can change to:

[HttpGet]
public async Task DbQuery()
{
            using (TransactionScope s = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                var result = await ExecuteStoredProcedure("usp_Procedure1", null).ConfigureAwait(false);
                //result = -1;
                var result2 = await ExecuteStoredProcedure("usp_Procedure2", result).ConfigureAwait(false);

                s.Complete();
            }

            return new ContentResult() { Content = "SP run done", ContentType = "text/html" };
}


private async Task ExecuteStoredProcedure(string spName, int? val)
{
            using (SqlConnection conn = new SqlConnection("Server=.;Initial Catalog=Test;Integrated Security=SSPI;"))
            {
                SqlCommand command = new SqlCommand();
                command.Connection = conn;
                command.CommandText = spName;
                command.CommandType = System.Data.CommandType.StoredProcedure;
                if (val.HasValue)
                {
                    command.Parameters.Add(new SqlParameter("Table1Id", val.Value));
                }

                conn.Open();
                using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false))
                {
                    if (reader.Read())
                    {
                        return Convert.ToInt32(reader["Identity"]);
                    }
                }
            }

            return -1;
}


I am sure that there would be some other scenarios like the above one where you may be forced to run the code blocks in single thread in blocking manner. However, there are workarounds too. For example - you can use Task.Run or use TaskCompletionSource in certain scenarios to make it favorable. The bottom line is to verify if the scenario is indeed not possible and if it is not then do not forget to remove Async/Await from upstream code blocks too to reduce false impressions.

No comments:

Post a Comment