Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Calling Result on an Async function cause block indefinitely?

I am kinda new to C# from a heavy java background, and still trying to figure out how exactly await and async works, here's whats confusing me a lot:

Suppose I have a function likes this (Hackish code below is used for experiments only):

public static bool CheckInternetConnection()
{
    Task<bool> result = CheckInternetConnectionAsync();
    return result.Result;
}

public async static Task<bool> CheckInternetConnectionAsync()
{
    if (NetworkInterface.GetIsNetworkAvailable())
    {
        SomeClass details = await ReturnARunningTask();
        return details.IsSuccessful;
    }
    return false;
}

public Task<SomeClass> ReturnARunningTask()
{
    return Task.Run(() => checkInternet());
}

in the main execution thread, if I call this:

CheckInternetConnection();

It will block indefinitely, my assumption is that control leaves CheckInternetConnectionAsync() at the "await" keyword and blocks on ".Result". When await resumes, the main thread is already blocked and stays blocked

The reason for my assumption is that I can see the task finish and return, but the code after await is never executed

however, if I do this:

bool result = Task.Run(() => CheckInternetConnection()).Result;

then the code after the await statement is executed and main thread does continue

My expectation was that it will also block, because the main thread would be blocked on the intermediate task,. the intermediate task would be blocked on the .Result

so..what made the difference? What in this case the code after await gets executed?

like image 222
Matthew Yang Avatar asked Feb 03 '15 01:02

Matthew Yang


1 Answers

In a UI environment you have a special single threaded SynchronizationContext that runs everything on the UI thread. That context is captured when you await a task and when the task completes the method resumes on that captured context (this can be configured using ConfigureAwait:

SomeClass details = await ReturnARunningTask().ConfigureAwait(false);

In your case when you use the Task.Result property on a not-completed task you are synchronously blocking the UI thread and so when the CheckInternetConnectionAsync tries to resume on the SynchronizationContext after ReturnARunningTask completed it can't. The UI thread is blocked waiting on the CheckInternetConnectionAsync task which in turn waits for the UI thread, hence deadlock (blocked indefinitely).

The difference when you use Task.Run(() => CheckInternetConnection()).Result; is that using Task.Run offloads work to be done on a ThreadPool thread. These threads don't have a SynchronizationContext and so while the UI thread is blocked on .Result the task can complete since it doesn't need the UI thread to do so.

Blocking on an asynchronous operation (sync over async) is discouraged since it blocks threads and makes the app less responsive and scalable and it can leads to deadlocks. You should instead use async-await all the way.


Note: Instead of doing "sync over async" you should have the synchronous version be synchronous and if you need an async version that offloads that CPU-intensive work to a different thread use Task.Run:

public static bool CheckInternetConnection()
{
    return checkInternet().IsSuccessful;
}

public async static Task<bool> CheckInternetConnectionAsync()
{
    if (NetworkInterface.GetIsNetworkAvailable())
    {
        return await Task.Run(() => CheckInternetConnection());
    }
    return false;
}
like image 109
i3arnon Avatar answered Sep 27 '22 18:09

i3arnon