Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy<Task<T>> with asynchronous initialization

Tags:

c#

async-await

class Laziness
{
    static string cmdText = null;
    static SqlConnection conn = null;

 
    Lazy<Task<Person>> person =
        new Lazy<Task<Person>>(async () =>      
        {
            using (var cmd = new SqlCommand(cmdText, conn))
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    string firstName = reader["first_name"].ToString();
                    string lastName = reader["last_name"].ToString();
                    return new Person(firstName, lastName);
                }
            }
            throw new Exception("Failed to fetch Person");
        });

    public async Task<Person> FetchPerson()
    {
        return await person.Value;              
    }
}

And the book, "Concurrency in .NET" by Riccardo Terrell, June 2018, says:

But there's a subtle risk. Because Lambda expression is asynchronous, it can be executed on any thread that calls Value and the expression will run within the context. A better solution is to wrap the expression in an underlying Task which will force the asynchronous execution on a thread pool thread.

I don't see what's the risk from the current code ?

Is it to prevent deadlock in case the code is run on the UI thread and is explicity waited like that:

new Laziness().FetchPerson().Wait();
like image 709
John Avatar asked Mar 04 '23 13:03

John


1 Answers

I don't see what's the risk from the current code ?

To me, the primary issue is that the asynchronous initialization delegate doesn't know what context/thread it'll run on, and that the context/thread could be different based on a race condition. For example, if a UI thread and a thread pool thread both attempt to access Value at the same time, in some executions the delegate will be run in a UI context and in others it will be run in a thread pool context. In the ASP.NET (pre-Core) world, it can get a bit trickier: it's possible for the delegate to capture a request context for a request that is then canceled (and disposed), and attempt to resume on that context, which isn't pretty.

Most of the time, it wouldn't matter. But there are these cases where Bad Things can happen. Introducing a Task.Run just removes this uncertainty: the delegate will always run without a context on a thread pool thread.

like image 165
Stephen Cleary Avatar answered Mar 11 '23 08:03

Stephen Cleary