John554
John554

Reputation: 303

C# Async deadlock

I am having trouble with a deadlock in C# using an async method from an external library. I am reasonably new to async programming so it's possible I have missed something very obvious here. The library I have been using is nethereum and the following code will hang without throwing any errors and I don't know how to debug it.

public Account(Wallet wallet, string name = "") //This is a constructor so it can't be async.
        {
            Console.WriteLine("##########################################################       Starting Account Constructor");
            
            Task.Run(async () => await GetTokens()); //If I change this to a .wait() it will hang one call earlier at TokenListService
            Console.WriteLine("##########################################################       Finishing Account Constructor");
        }

        public async Task GetTokens()
        {
            Console.WriteLine("##########################################################       Starting Account GetTokens");
            Web3 web3 = new Web3(Globals.web3Endpoint);
            Console.WriteLine("##########################################################       Starting Account LoadFromUrl");
            var tokens = await new TokenListService().LoadFromUrl(TokenListSources.UNISWAP);
            Console.WriteLine("##########################################################       Starting Account GetAllTokenBalancesUsingMultiCallAsync"); 

            //Hangs here
            var tokensOwned = await web3.Eth.ERC20.GetAllTokenBalancesUsingMultiCallAsync(
                    new string[] { privateKey }, tokens.Where(x => x.ChainId == 1),
                    BlockParameter.CreateLatest());

            //This never runs
            Console.WriteLine("##########################################################       Starting Account GetTotalBalance");
}

Additionally to the above the code works when I run it in a stand alone console application. The below example should run fine.

namespace testProj
{
    class Program
    {
        public static async Task GetERC20Balances()
        {
            var web3 = new Web3(“<endpoint>”);
            var tokens = await new TokenListService().LoadFromUrl(TokenListSources.UNISWAP);

            var owner = “<account address>”;
            var tokensOwned = await web3.Eth.ERC20.GetAllTokenBalancesUsingMultiCallAsync(
                    new string[] { owner }, tokens.Where(x => x.ChainId == 1),
                    BlockParameter.CreateLatest());

            Console.WriteLine(“This works…”);
        }

        static void Main()
        {
            GetERC20Balances().Wait();
            Console.WriteLine("Finished!");
            Console.ReadLine();
        }
    }
}

How would I go about debugging this deadlock?

Upvotes: 0

Views: 1538

Answers (2)

ToolmakerSteve
ToolmakerSteve

Reputation: 21213

This answer is specific to UI MVVM coding. To show a way to apply the accepted answer.

This is in response to .NET MAUI App Crashes on Navigating to a Page with Data Binding.

This attempt at MVVM causes a deadlock (app UI freezes):

// Code-behind of a Maui page:
public partial class SomePage : ContentPage
{
    public SomePage()
    {
        InitializeComponent();
        BindingContext =new SomePageViewModel();
    }
}
// ViewModel:
public partial class SomePageViewModel : ObservableObject
{
    public SomePageViewModel()
    {
        // CAUSES DEADLOCK: attempt to call async method from constructor on UI thread.
        Initialize();
    }
    
    private async void Initialize()
    {
        ...
        var client = new HttpClient();
        ... = await client.GetAsync(url);   // SLOW: web server Request/Response.
        ...
    }
}

TO AVOID DEADLOCK

  • DO NOT call async methods without await. SomePageViewModel() calls such a method.
  • You can't put await except inside an async context, so refactor the code. Best is to move calls to async method(s) out of the constructor.
  • One solution is a "static async factory method":
// Code-behind of a Maui page:
public partial class SomePage : ContentPage
{
    public SomePage()
    {
        InitializeComponent();
        // DO NOT set BindingContext here. This is done later.
    }

    private bool isInitialized;
     
     // This is method used by Maui. Other UI frameworks may have a different name.
     protected override async void OnAppearing()
     {
         base.OnAppearing();

         // OnAppearing is also called when we go back to page later.
         // This flag avoids initializing again.
         if (!isInitialized)
         {
             isInitialized = true;
             await SetBindingContext();
         }
     }
     
     private async Task SetBindingContext()
     {
        // The page may appear empty, until BindingContext is set.
        // [Beyond scope of answer: show a busy indicator while data is fetched.]
        var vm = await SomePageViewModel.Create();
        // ...set vm properties here as needed...
        BindingContext = vm;
     }
}
// ViewModel:
public partial class SomePageViewModel : ObservableObject
{
    // "static async factory method".
    // We do this instead of the "Task.Run" solution shown above,
    // because caller wants the object AFTER Initialize has run.
    public static async SomePageViewModel Create()
    {
        var it = new SomePageViewModel();
        await it.Initialize();
        return it;
    }
    
    public SomePageViewModel()
    {
    }
    
    private async void Initialize()
    {
        ...
        var client = new HttpClient();
        ... = await client.GetAsync(url);   // SLOW: web server Request/Response.
        ...
    }
}

See also:

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 456332

I am reasonably new to async programming so it's possible I have missed something very obvious here.

There's a couple common guidelines with async:

These are guidelines, not hard-and-fast rules, but in general if you follow them you'll have less pain.

//This is a constructor so it can't be async.

That's correct. So you'll need to do something else. Somehow or another you'll have to either break the guideline (i.e., block on the asynchronous code), or restructure the code so that the constructor does not need to call asynchronous code. In this case (and in most cases), I recommend the latter.

Since this is a constructor, and since you're observing a deadlock, I'm assuming that you are writing a GUI application. If that assumption is correct, then blocking on asynchronous code - while possible using a hack like the thread pool hack - is not recommended; blocking the UI thread results in a poor user experience.

A better solution for GUI applications is to immediately (and synchronously) initialize the UI into a "loading" state, and start the asynchronous operation. Then, when the operation completes, update the UI into its normal "data display" state. The code is more complex, but it does provide a better user experience (and also avoids the OS complaining that your app is hung).

If this is a XAML GUI app, then I have an article that explains how to set up a "notifying task" object. Your code then starts the operation when it creates that object, and can use data binding to update its UI when the operation completes. Similar patterns can be used for other GUI application frameworks.

Upvotes: 3

Related Questions