C# Azure存储表访问存在无法解释的异步/等待问题-是否可以使用ConfigureAwait解决(错误)?可能不会

C# Azure存储表访问存在无法解释的异步/等待问题-是否可以使用ConfigureAwait解决(错误)?可能不会,c#,azure,asp.net-web-api,async-await,azure-table-storage,C#,Azure,Asp.net Web Api,Async Await,Azure Table Storage,我正在进行部署到Azure的新ASP.NET Framework WebApi应用的第三个月的工作 我不需要保存那么多数据,但我保存的数据在Azure存储表中 大约一周前,在几周没有问题之后,我开始在异步/等待同步方面遇到麻烦,这似乎是出乎意料的。我能够将该问题定位到等待异步执行对Azure存储表的访问。下面是我的应用程序工作原理的简化示意图: using System.Threading.Tasks; using System.Web.Hosting; using System.Web.Htt

我正在进行部署到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.
    }
}
要重申代码段注释中的内容,有时但并非总是使用
CloudTable.ExcecuteAsync()
方法访问存储表将永远挂起,这表明存在死锁,但如果我将
.ConfigureAwait(false)
附加到调用中,它总是可以正常工作

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

关于这些问题:

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

  • 以一种不太令人满意的方式回答我自己的问题,我不会投赞成票,也不会把它作为答案

    多亏了对我最初问题和后续研究的评论,我真的不喜欢
    .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();
            }
        }
    }
    
    使用System.Collections.Generic;
    使用系统线程;
    使用System.Threading.Tasks;
    使用Microsoft.WindowsAzure.Storage.Table;
    公共静态类CloudTableExtension
    {
    /// 
    ///黑客保存包装执行异步
    /// 
    ///云台
    ///表操作
    ///取消令牌
    ///底层ExecuteAsync()的结果
    /// 
    ///而不是使用.ConfigureAwait(false)包装对ExecuteAsync()的调用,因此不使用当前同步上下文,
    ///我正在强制响应后面跟着Task.Yield()。一旦我能够前进,我就可以停止使用此包装
    ///到最新版本的Azure存储SDK。
    /// 
    公共静态异步任务HackedSafeWrappedExecuteAsync(此CloudTable CloudTable、TableOperation TableOperation、CancellationToken?CancellationToken=null)
    {
    尝试
    {
    返回等待(cancellationToken==null?cloudTable.ExecuteAsync(tableOperation):cloudTable.ExecuteAsync(tableOperation,cancellationToken.Value));
    }
    最后
    {
    等待任务;
    }
    }
    /// 
    ///黑客安全包装执行批异步
    /// 
    ///云台
    ///表批处理操作
    ///取消令牌
    ///底层ExecuteBatchAsync的结果
    /// 
    ///而不是使用.ConfigureAwait(false)包装对ExecuteBatchAsync()的调用,因此不使用当前同步上下文,
    ///我正在强制响应后面跟着Task.Yield()。一旦我能够前进,我就可以停止使用此包装
    ///到最新版本的Azure存储SDK。
    /// 
    公共静态异步任务HackedSafeWrappedExecuteBatchAsync(此CloudTable CloudTable、TableBatchOperation TableBatchOperation、CancellationToken?CancellationToken=null)
    {
    尝试
    {
    返回等待(cancellationToken==null?cloudTable.ExecuteBatchAsync(tableBatchOperation):cloudTable.ExecuteBatchAsync(tableBatchOperation,cancellationToken.Value));
    }
    最后
    {
    等待任务;
    }
    }
    }
    
    “还好吗?”--嗯,您有一个无法解释的问题。所以,不。通过在问题上撒
    ConfigureAwait(false)
    fairy灰尘来“修复”问题是不好的。首先你需要解释这个问题。只有了解问题后,才能继续进行有效的修复
    ConfigureAwait(false)
    very可能是正确的修复方法;这是处理同步上下文死锁时的常见解决方案,在许多情况下是正确的解决方案。但是当你不知道真正的问题是什么的时候?不,你不能把它藏在地毯下面。你必须在修复之前调试它。是的,谢谢@PeterDuniho-我同意你的观点,你说的比我更有力、更有用。经过几天的代码筛选,我只是不理解,这就是我写这篇文章的原因。我很感激。老实说,我想我也在寻找那些知道“哦,对了,这是存储表访问的一个已知问题”的人。在一个大的代码库中发现死锁可能很棘手,但只要有耐心,您应该能够找到它。您正在寻找一个阻塞调用,它会占用您的同步上下文,并且