问题描述
我正在开发部署到 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/await。 - 鉴于我能够通过使用
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();
}
}
}