Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是

Posted

技术标签:

【中文标题】Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是【英文标题】:Unexplained async/await problem with Azure Storage Table access - is it OK to solve with ConfigureAwait(false)? Probably NOT 【发布时间】:2021-07-22 05:51:01 【问题描述】:

我正在开发部署到 Azure 的新 ASP.NET Framework WebApi 应用程序的第三个月。

我不需要保留那么多数据,但我确实保留的数据是在 Azure 存储表中。

大约一周前,在几周没有问题之后,我开始遇到异步/等待同步问题,这似乎是出乎意料的。我能够将该问题本地化为等待异步执行对 Azure 存储表的访问。这是我的应用程序如何工作的非常简化的示意图:

using System.Threading.Tasks;
using System.Web.Hosting;
using System.Web.Http;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;

public class DummyController : ApiController

    public async Task Post()
    
        string payloadDescribingWork = await Request.Content.ReadAsStringAsync();  // Await here - request is disposed before async task queued.

        // Service that hooks by posting to me needs a 204 response immediately,
        // which is why I queue a background work item for the real work.
        // Background work item will never take longer than 30 seconds,
        // but caller will time out if I don't respond 
        HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
        
            await Task.Delay(3000, cancellationToken); // Simulate some work based on the payload above

            CloudStorageAccount storageAccount = CloudStorageAccount.Parse("MyConnectionString");
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable table = tableClient.GetTableReference("MyTableName");
            table.CreateIfNotExists();

            // Sometimes but not always, this next awaitable async insert operation will NEVER return
            // In that case the background work item will never complete and will only
            // ever go away when IIS cycles the thread pool.
            // However, if you look at the table with a table explorer, the row actually WAS successfully
            // inserted, even when this operation hangs.
            TableResult noConfigureAwaitResult = 
                await table.ExecuteAsync(TableOperation.Insert(new TableEntity
                
                    PartitionKey = "MyPartitionKey",
                    RowKey = "MyRowKey"
                ), cancellationToken);

            // The following awaitable async insert operation wrapped with "ConfigureAwait(false)"
            // will always return and always succeed.
            TableResult configureAwaitFalseResult = 
                await table.ExecuteAsync(TableOperation.Insert(new TableEntity
                
                    PartitionKey = "MyOtherPartitionKey",
                    RowKey = "MyOtherRowKey"
                ), cancellationToken).ConfigureAwait(false);
        );

        // 204 response will be issued right away here by the web api framework.
    

重申一下 sn-p 的 cmets 中的内容,有时但并非总是使用 CloudTable.ExcecuteAsync() 方法对存储表的访问将永远挂起,这表明出现死锁,但如果我将 .ConfigureAwait(false) 附加到调用,它总是工作正常。

问题是我不明白为什么。当然,让我的代码正常工作感觉很好,但这可能掩盖了更深层次的问题。

所以问题:

    鉴于我的实际排队后台工作要复杂得多,任何人都想冒险猜测为什么存储表访问有时会在未使用.ConfigureAwait(false) 包裹时挂起?请注意,我已对我的应用程序进行了详尽的审核,以确保我在调用堆栈上下一致地使用 async/await。 鉴于我能够通过使用 ConfigureAwait(false) 包装所有 Azure 存储访问操作来让我的应用程序正常工作,是否有人认为这从长远来看可能是一个糟糕的解决方案?

【问题讨论】:

“可以吗?” -- 好吧,你有一个无法解释的问题。所以不行。通过将ConfigureAwait(false) 仙尘洒在上面来“解决”问题是不行的。首先,您需要解释问题。只有在了解问题后,您才能进行有效的修复。 ConfigureAwait(false) 很可能是正确的解决方法;这是处理同步上下文死锁时的常见解决方案,并且在许多情况下是正确的解决方案。但是当您不知道实际问题是什么时?不……你不能把它扫到地毯下面。在修复它之前,您必须对其进行调试。 是的,谢谢@PeterDuniho - 我同意你的观点,你说的比我更有力和更有帮助。经过几天的代码筛选后,我只是不理解,这就是我写这篇文章的原因。我很感激。老实说,我想我也在寻找任何知道“哦,对了,这是存储表访问的已知问题”的人。 在大型代码库中很难找到死锁,但只要有耐心,您应该能够找到它。您正在寻找一个阻塞同步上下文的 blocking 调用,它正在等待其他一些等待的异步结果完成。专注于阻塞调用......它们通常会出现在层之间的一些过渡中(即有人认为到目前为止只将异步推高就可以逃脱),但无论如何,它们通常会像拇指酸痛一样突出.祝你好运。 不要在控制器操作方法中使用ConfigureAwait(false)。这就是需要SynchronizationContext 的地方。 嗨@PauloMorgado - 谢谢,但请注意它在一个单独的工作线程中,不是控制器操作的一部分,在传递给HostingEnvironment.QueueBackgroundWorkItem的委托内。 【参考方案1】:

以不太令人满意的方式回答我自己的问题,我不会投票给它或将其标记为答案。

感谢 cmets 对我最初的问题和随后的研究,我真的不喜欢 .ConfigureAwait(false) 解决方案。

不过我梳理了代码,并没有发现死锁,相信可能是存储表代码有问题。我应该说我使用的是来自 NuGet 的旧版 SDK,并且由于我的代码中的其他依赖项无法轻松升级,但也许当我可以重构该升级时,问题就会消失。但是,就目前而言,我已经找到了一个包装器,我可以将它放在我的存储表调用周围,它可以让我的代码在所有情况下都完成。我仍然不确定为什么,但我不喜欢切换同步上下文。当然这里有性能损失,但现在我会接受它。

这是我的包装:

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Table;

public static class CloudTableExtension

    /// <summary>
    /// Hacked Save Wrapped Execute Async
    /// </summary>
    /// <param name="cloudTable">Cloud Table</param>
    /// <param name="tableOperation">Table Operation</param>
    /// <param name="cancellationToken">Cancellation Token</param>
    /// <returns>Result of underlying ExecuteAsync()</returns>
    /// <remarks>
    /// Rather than wrapping the call to ExecuteAsync() with .ConfigureAwait(false) and hence not using the current Synchronization Context,
    /// I am forcing the response to be followed with Task.Yield().  I may be able to stop use of this wrapper once I am able to advance
    /// to the newest release of the Azure Storage SDK.
    /// </remarks>
    public static async Task<TableResult> HackedSafeWrappedExecuteAsync(this CloudTable cloudTable, TableOperation tableOperation, CancellationToken? cancellationToken = null)
    
        try
        
            return await (cancellationToken == null ? cloudTable.ExecuteAsync(tableOperation) : cloudTable.ExecuteAsync(tableOperation, cancellationToken.Value));
        
        finally
        
            await Task.Yield();
        
    

    /// <summary>
    /// Hacked Safe Wrapped Execute Batch Async
    /// </summary>
    /// <param name="cloudTable">Cloud Table</param>
    /// <param name="tableBatchOperation">Table Batch Operation</param>
    /// <param name="cancellationToken">Cancellation Token</param>
    /// <returns>Result of underlying ExecuteBatchAsync</returns>
    /// <remarks>
    /// Rather than wrapping the call to ExecuteBatchAsync() with .ConfigureAwait(false) and hence not using the current Synchronization Context,
    /// I am forcing the response to be followed with Task.Yield().  I may be able to stop use of this wrapper once I am able to advance
    /// to the newest release of the Azure Storage SDK.
    /// </remarks>
    public static async Task<IList<TableResult>> HackedSafeWrappedExecuteBatchAsync(this CloudTable cloudTable, TableBatchOperation tableBatchOperation, CancellationToken? cancellationToken = null)
    
        try
        
            return await (cancellationToken == null ? cloudTable.ExecuteBatchAsync(tableBatchOperation) : cloudTable.ExecuteBatchAsync(tableBatchOperation, cancellationToken.Value));
        
        finally
        
            await Task.Yield();
        
    

【讨论】:

以上是关于Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是的主要内容,如果未能解决你的问题,请参考以下文章

如何异步执行 Azure 表存储查询?客户端版本 4.0.1

Azure 函数无法访问 Azure Blob

使用 Azure MSI 访问 Azure 表存储

Azure 表存储 - 查询时间戳

presto访问 Azure blob storage

无法通过端口 445 访问 Azure 存储帐户