C# 异步死锁

C# Async deadlock

我在使用外部库的异步方法时遇到 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();
        }
    }
}

我该如何调试这个死锁?

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

有一些关于 async 的通用指南:

这些是指南,而不是hard-and-fast规则,但一般来说,如果您遵循它们,您就会减轻痛苦。

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

没错。所以你需要做点别的。不知何故,您将不得不打破准则(即阻塞异步代码),或者重构代码,以便构造函数不需要调用异步代码。在这种情况下(在大多数情况下),我推荐后者。

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

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

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