Azure存储表访问出现不明原因的异步/等待问题-可以使用ConfigureAwait解决吗(false)?可能不会



我正在开发一个新的ASP.NET Framework WebApi应用程序,该应用程序已部署到Azure。

我不需要保存那么多数据,但我保存的数据在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 Storage Access操作来让我的应用程序正常工作,有人会争论为什么从长远来看这可能是一个糟糕的解决方案吗

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

由于对我最初的问题和随后的研究的评论,我真的不喜欢.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();
}
}
}

相关内容

最新更新