C# 异步死锁

问题描述 投票:0回答:2

我在使用外部库中的异步方法的 C# 中遇到死锁问题。我对异步编程相当陌生,所以我可能在这里错过了一些非常明显的东西。我一直在使用的库是 nethereum,下面的代码将挂起而不会抛出任何错误,而且我不知道如何调试它。

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");
}

除上述之外,当我在独立的控制台应用程序中运行代码时,该代码也可以工作。下面的示例应该运行良好。

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();
        }
    }
}

我该如何调试这个死锁?

c# asynchronous async-await
2个回答
2
投票

我对异步编程相当陌生,所以我可能在这里错过了一些非常明显的东西。

有一些与

async
相关的共同准则:

这些是指南,不是硬性规定,但一般来说,如果您遵循它们,您会减少痛苦。

//这是一个构造函数,所以它不能是异步的。

是的。所以你需要做点别的事情。无论如何,您必须打破准则(即阻止异步代码),或者重新构造代码,以便构造函数不需要调用异步代码。在这种情况下(并且在大多数情况下),我推荐后者。

因为这是一个构造函数,并且由于您正在观察死锁,所以我假设您正在编写一个 GUI 应用程序。如果这个假设是正确的,那么不建议阻塞异步代码 - 虽然可以使用像线程池 hack 这样的 hack ;阻塞 UI 线程会导致用户体验不佳。

GUI 应用程序更好的解决方案是立即(同步)将 UI 初始化为“正在加载”状态,并启动异步操作。然后,当操作完成时,更新 UI 进入正常的“数据显示”状态。代码更复杂,但它确实提供了更好的用户体验(并且还避免了操作系统抱怨您的应用程序挂起)。

如果这是一个 XAML GUI 应用程序,那么我有一篇 article 解释如何设置“通知任务”对象。然后,您的代码在创建该对象时启动该操作,并可以在操作完成时使用数据绑定来更新其 UI。类似的模式可用于其他 GUI 应用程序框架。


0
投票

添加特定于 UI MVVM 编码的答案。

这是为了响应.NET MAUI 应用程序在导航到具有数据绑定的页面时崩溃

考虑到 MVVM 的这种尝试:

// 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.
        ...
    }
}

死锁

  • 请勿在没有
    async
    的情况下调用
    await
    方法。
    CreateProductPageViewModel()
    调用这样的方法。
  • 您不能将
    await
    放入构造函数中,因此请重构代码。将对异步方法的调用移出构造函数。
    • 一种解决方案是“静态异步工厂方法”:
// Code-behind of a Maui page:
public partial class SomePage : ContentPage
{
    public SomePage()
    {
        InitializeComponent();
        
        Task.Run( async () =>
        {   // This code runs AFTER constructor returns.
            // The page will appear empty, until "Create" returns, and BindingContext is set.
            // [Beyond scope of answer: show a busy indicator while data is fetched.]
            var vm = new SomePageViewModel();
            await SomePageViewModel.Create();
            BindingContext = vm;
        });
     }
}
// ViewModel:
public partial class SomePageViewModel : ObservableObject
{
    public static async SomePageViewModel Create()
    {
        var it = new SomePageViewModel();
        await it.Initialize();   // "Init" or "Initialize" would be good name here.
        return it;
    }
    
    public SomePageViewModel()
    {
    }
    
    private async void Initialize()
    {
        ...
        var client = new HttpClient();
        ... = await client.GetAsync(url);   // SLOW: web server Request/Response.
        ...
    }
}

另请参阅:

© www.soinside.com 2019 - 2024. All rights reserved.