Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是
Posted
技术标签:
【中文标题】Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是【英文标题】:Unexplained async/await problem with Azure Storage Table access - is it OK to solve with ConfigureAwait(false)? Probably NOT 【发布时间】:2021-07-22 05:51:01 【问题描述】:我正在开发部署到 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.
重申一下 sn-p 的 cmets 中的内容,有时但并非总是使用 CloudTable.ExcecuteAsync()
方法对存储表的访问将永远挂起,这表明出现死锁,但如果我将 .ConfigureAwait(false)
附加到调用,它总是工作正常。
问题是我不明白为什么。当然,让我的代码正常工作感觉很好,但这可能掩盖了更深层次的问题。
所以问题:
-
鉴于我的实际排队后台工作要复杂得多,任何人都想冒险猜测为什么存储表访问有时会在未使用
.ConfigureAwait(false)
包裹时挂起?请注意,我已对我的应用程序进行了详尽的审核,以确保我在调用堆栈上下一致地使用 async/await。
鉴于我能够通过使用 ConfigureAwait(false)
包装所有 Azure 存储访问操作来让我的应用程序正常工作,是否有人认为这从长远来看可能是一个糟糕的解决方案?
【问题讨论】:
“可以吗?” -- 好吧,你有一个无法解释的问题。所以不行。通过将ConfigureAwait(false)
仙尘洒在上面来“解决”问题是不行的。首先,您需要解释问题。只有在了解问题后,您才能进行有效的修复。 ConfigureAwait(false)
很可能是正确的解决方法;这是处理同步上下文死锁时的常见解决方案,并且在许多情况下是正确的解决方案。但是当您不知道实际问题是什么时?不……你不能把它扫到地毯下面。在修复它之前,您必须对其进行调试。
是的,谢谢@PeterDuniho - 我同意你的观点,你说的比我更有力和更有帮助。经过几天的代码筛选后,我只是不理解,这就是我写这篇文章的原因。我很感激。老实说,我想我也在寻找任何知道“哦,对了,这是存储表访问的已知问题”的人。
在大型代码库中很难找到死锁,但只要有耐心,您应该能够找到它。您正在寻找一个阻塞同步上下文的 blocking 调用,它正在等待其他一些等待的异步结果完成。专注于阻塞调用......它们通常会出现在层之间的一些过渡中(即有人认为到目前为止只将异步推高就可以逃脱),但无论如何,它们通常会像拇指酸痛一样突出.祝你好运。
不要在控制器操作方法中使用ConfigureAwait(false)
。这就是需要SynchronizationContext
的地方。
嗨@PauloMorgado - 谢谢,但请注意它在一个单独的工作线程中,不是控制器操作的一部分,在传递给HostingEnvironment.QueueBackgroundWorkItem
的委托内。
【参考方案1】:
以不太令人满意的方式回答我自己的问题,我不会投票给它或将其标记为答案。
感谢 cmets 对我最初的问题和随后的研究,我真的不喜欢 .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();
【讨论】:
以上是关于Azure 存储表访问出现无法解释的异步/等待问题 - 可以使用 ConfigureAwait(false) 解决吗?可能不是的主要内容,如果未能解决你的问题,请参考以下文章