C# Azure存储表访问存在无法解释的异步/等待问题-是否可以使用ConfigureAwait解决(错误)?可能不会
我正在进行部署到Azure的新ASP.NET Framework WebApi应用的第三个月的工作 我不需要保存那么多数据,但我保存的数据在Azure存储表中 大约一周前,在几周没有问题之后,我开始在异步/等待同步方面遇到麻烦,这似乎是出乎意料的。我能够将该问题定位到等待异步执行对Azure存储表的访问。下面是我的应用程序工作原理的简化示意图: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
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/WaitConfigureAwait(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-我同意你的观点,你说的比我更有力、更有用。经过几天的代码筛选,我只是不理解,这就是我写这篇文章的原因。我很感激。老实说,我想我也在寻找那些知道“哦,对了,这是存储表访问的一个已知问题”的人。在一个大的代码库中发现死锁可能很棘手,但只要有耐心,您应该能够找到它。您正在寻找一个阻塞调用,它会占用您的同步上下文,并且