Rodrigo Ferreira
Rodrigo Ferreira

Reputation: 387

C# asynchronous functions - does await immediately start a task on a new thread?

I'm refactoring some C# code for asynchronous operation, but I'm afraid I'm not understanding in depth what is going on with the C# await instructions. I have a method that does some potentially lengthy processing and will need to run 200 times in a row:

public LanDeviceInfo GetLanDBData(LanDeviceInfo device) 

And I create an asynchronous version which uses it:

public async Task<LanDeviceInfo> GetLanDBDataAsync(LanDeviceInfo device)
{
    var deviceInfo = await Task.Run(() => GetLanDBData(device));
    return deviceInfo;
}

And, on the UI, on the click of a button, I'm running it with the await keyword inside of a loop:

    private async void GenerateCommissioningFile()
    {
            foreach (VacFwPLCInfo thisPLC in filteredPLCList)
            {
                try
                {
                    plcCount++;
                    buttonGenerateComFile.Text = $"LanDB ({plcCount}/{filteredPLCList.Count})";
                    await lanDB.GetLanDBDataAsync(thisPLC);
                }
                catch (Exception ex)
                {
                    thisPLC.Error = true;
                }
            }
    }

All of this works fine, the function gets called in an asynchronous way and my UI doesn't get blocked.

Now, what I don't understand is why defining the GetLanDBDataAsync function as follows compiles fine but doesn't work and blocks the UI thread:

public async Task<LanDeviceInfo> GetLanDBDataAsync(LanDeviceInfo device)
{
    return GetLanDBData(device);
}

From what I understand, this should work as well. Defining this function using the async modifier with the Task return type would make the compiler automatically generate a task which would be returned whenever GetLanDBDataAsync() is called. Then calling await GetLanDBDataAsync() from GenerateCommissioningFile() would automatically make it run in a new thread and not block the UI. Why must I manually create a task to run GetLanDBData() and await it inside GetLanDBDataAsync() when GenerateCommissioningFile(), which runs on the UI thread, is already awaiting the asynchronous function? I feel like I'm really missing something here ;)

Thanks!

Upvotes: 1

Views: 2264

Answers (1)

Evk
Evk

Reputation: 101483

For this function:

public async Task<LanDeviceInfo> GetLanDBDataAsync(LanDeviceInfo device)
{
    return GetLanDBData(device);
}

compiler should issue a warning: "this async method lacks 'await' operators and will run synchronously". Compiler will convert this method to something like this:

public Task<LanDeviceInfo> GetLanDBDataAsync(LanDeviceInfo device)
{
    var result = GetLanDBData(device);
    return Task.FromResult(result);
}

So indeed compiler generated a task and returned it, but not in a way you expected. The whole method runs synchronously on caller (UI) thread and so blocks it the same way as GetLanDBData does.

Task basically represents some work in progress (or even already completed, as shown above with Task.FromResult), with ability to check the status of said work, and being notified when work is completed (or failed). It doesn't necessary has any relation to threads.

await someTask very rougly means - IF someTask is not already completed - then execute the rest of the method some time later, when someTask actually completes. It doesn't start any new tasks, nor does it create any new threads.

Your working version:

public async Task<LanDeviceInfo> GetLanDBDataAsync(LanDeviceInfo device)
{
    var deviceInfo = await Task.Run(() => GetLanDBData(device));
    return deviceInfo;
}

very rougly means -

  1. create a task representing the whole operation inside GetLanDBDataAsync method. Let's name that taskResult.

  2. Queue GetLanDBData to be executed on thread pool (because that's what documentation of Task.Run says it does, not just because "it's a task"). Task returned from Task.Run represents this pending operation.

  3. Now, if task returned from Task.Run is not yet completed (it does not) - return our taskResult (representing whole operation) back to the caller.

  4. When some time later task returned from Task.Run completes - we execute the rest of the code. In this case, result of Task.Run is just forwarded to our taskResult, because the rest of the code is just return deviceInfo.

Upvotes: 3

Related Questions