微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

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

我正在开发部署到 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)到电话,它总是工作正常。

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

对于问题:

  1. 鉴于我实际排队的后台工作要复杂得多,任何人都想冒险猜测为什么在没有用 .ConfigureAwait(false) 包裹时存储表访问有时会挂起?请注意,我已经通过我的应用程序进行了每一次详尽的审核,以确保我在调用堆栈上下一致地使用 async/await。
  2. 鉴于我能够通过使用 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();
        }
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。