这是我在一些遗留代码中遇到的怪异代码,我已经将其标记为用于重构。我已经检查了一下,并且代码未使用.ConfigureAwait(false)
。
有问题的代码看起来像这样:(“ testx = ....”行是调试问题以暴露行为的一部分。)
public async Task<ActionResult> Index()
{
ValidateRoleAccess(Roles.Admin, Roles.AuthorizedUser, Roles.AuditReadOnly);
var test1 = System.Web.HttpContext.Current != null
var decisions = await _lookupService.GetAllDecisions();
var test2 = System.Web.HttpContext.Current != null
var statuses = await _lookupService.GetAllEnquiryStatuses();
var test3 = System.Web.HttpContext.Current != null
var eeoGroups = await _lookupService.GetEEOGroups();
var test4 = System.Web.HttpContext.Current != null
var subCategories = await _lookupService.GetEnquiryTypeSubCategories();
var test5 = System.Web.HttpContext.Current != null
var paystreams = await _lookupService.GetPaystreams();
var test6 = System.Web.HttpContext.Current != null
var hhses = await _lookupService.GetAllHHS();
var test7 = System.Web.HttpContext.Current != null
// ...
调用本身是通过同一查找服务针对EF的简单查询。假设它们是EF并且使用相同的UoW / DbContext,则不能将它们更改为使用Task.WhenAll()
。
预期结果: 适用于test1-> test7
实际结果: 对于test1为True,-> test3,对于test4为False-> test7
当我在等待的查找调用之后添加针对特定角色的验证时,发现了该问题。该检查在验证方法使用的HttpContext.Current上触发了一个空引用异常。因此,它在异步之前在ValidateRoleAccess调用中使用时通过了,但在所有等待的方法之后调用时失败了。
我改变了方法的顺序,但在等待2或3次后都失败了,没有特别的罪魁祸首。该应用的目标是.Net 4.6.1。这是一个非阻塞性问题,因为我能够在等待之前执行角色检查,将结果放入变量中,并在等待之后引用该变量,但这是一个非常意外的“陷阱”,无法在1-之后工作2等待,但不更多。由于这些查找不需要异步调用,也不会返回整个实体,因此代码将得到重构,但是我仍然很好奇是否有解释为什么在几遍之后HttpContext会“丢失”的原因未使用带有.ConfigureAwait(false)的等待任务。
更新1:地块变厚。 我调整了测试调用,以将测试布尔结果添加到列表中,然后重复进行5次迭代的加载。我想看看它是否曾经在某个时候返回“ True”,是否跳到了“ False”。我的想法是,等待是在没有引用当前HttpContext的另一个线程上继续进行的,但是无论我添加了多少次迭代(一旦为false),它总是为false。因此,接下来我尝试将第一次调用(GetAllDecisions)重复10次...令人惊讶的是,前10次迭代全部返回为True ?!因此,我更仔细地研究了更改顺序,以查看是否有可靠地将其触发的呼叫,结果发现其中有3个。
例如:
var decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
decisions = await _lookupService.GetAllDecisions();
results.Add(System.Web.HttpContext.Current != null);
返回True,True,True,但随后将其更改为:
var eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
eeoGroups = _lookupService.GetEEOGroups();
results.Add(System.Web.HttpContext.Current != null);
返回False,False,False。
深入研究,我注意到这些方法是EntityFramework和较旧的基于NHibernate的存储库代码的混合。正是EntityFramework异步方法在等待时触发了上下文。
等待后触发Context的方法之一:
public async Task<List<string>> GetEEOGroups()
{
return await _dbContext.EmployeeEEOGroup.GroupBy(e => e.EEOGroup).Select(g => g.FirstOrDefault().EEOGroup).ToListAsync();
}
一样:*编辑-whups,它是重复的副本/粘贴:)
public async Task<IEnumerable<SapHHS>> GetAllHHS()
{
return await _dbContext.HHS.Where(x => x.IsActive).ToListAsync();
}
这还好:
public async Task<IEnumerable<Decision>> GetAllDecisions()
{
return await Task.FromResult(_repository.Session.QueryOver<Lookup>().Where(l => l.Type == "Decision" && l.IsActive).List().Select(l => new Decision { DecisionId = l.Id, Description = l.Name }).ToList());
}
查看“有效”的代码,很明显,在给定Task.FromResult针对同步方法的情况下,它实际上并未执行任何异步操作。我认为原始作者被异步子弹的魅力所吸引,只是为了保持一致性而包装了较旧的代码。 EF的async方法可与await一起使用,但是HttpContext似乎支持async / await。当前只要<httpRuntime targetFramework="4.5" />
,EF似乎都会使这一假设无效。
答案 0 :(得分:2)
在blogs msdn中,有一个关于此问题的完整研究以及该问题的映射,即问题根源:
HttpContext对象存储所有与请求相关的数据,包括 指向本机IIS请求对象,ASP.NET管道的指针 实例,请求和响应属性,会话(以及许多 其他)。如果没有所有这些信息,我们可以安全地告诉我们的代码 不知道它正在执行的请求上下文。设计中 完全无状态的Web应用程序并不容易,要实现它们 确实是一项艰巨的任务。而且,Web应用程序丰富了 第三方库,通常是黑匣子,其中 未指定的代码运行。
要思考的是,在请求执行期间如何丢失程序中如此重要的基础对象HttpContext
是一件非常有趣的事情。
提供的灵魂
它由TaskScheduler
,ExecutionContext
和SynchronizationContext
组成:
TaskScheduler
正是您所期望的。任务调度器! (我会是一个很棒的老师!)取决于.NET的类型
应用程序,特定的任务计划程序可能比
其他。默认情况下,ASP.NET使用ThreadPoolTaskScheduler,即 针对吞吐量和并行后台处理进行了优化。ExecutionContext
(EC)再次与名称所暗示的相似。您可以将其视为TLS(本地线程 存储),用于多线程并行执行。在极端综合中,
它是用于保留所有所需环境上下文的对象 来确保代码可以运行
在不同线程上中断和恢复而不会造成伤害(均来自
逻辑和安全角度)。要理解的关键方面是
EC需要“流动”(基本上是从线程复制到
另一种),只要发生代码中断/恢复。SynchronizationContext
(SC)有点难以掌握。它与EC相关并在某些方面相似,尽管 加强更高的抽象层。确实可以持续
环境状态,但具有专用的实现方式
在特定环境中对工作项进行排队/出队。感谢 SC,开发人员可以编写代码而不必担心
运行时处理异步/等待模式。
如果您考虑博客示例中的代码:
await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait);
await Task.Factory.StartNew(
async () => await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait),
System.Threading.CancellationToken.None,
asyncContinue ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None,
tsFromSyncContext ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Current)
.Unwrap().ConfigureAwait(configAwait);
曝光:
configAwait
:控制等待任务时的ConfigureAwait行为(请继续阅读以获取其他注意事项)tsFromSyncContext
:控制传递给StartNew方法的TaskScheduler选项。如果为true,则TaskScheduler是从当前版本构建的 SynchronizationContext,否则使用当前TaskScheduler。doSleep
:如果为True,则DoStuff等待Thread.Sleep。如果为False,它将在HttpClient.GetAsync操作上等待。如果要测试,则有用
它没有互联网连接asyncContinue
:控制传递给StartNew方法的TaskCreationOptions。如果为true,则继续以异步方式运行。
如果您还计划测试继续任务并评估
嵌套等待操作中任务内联的行为
(不会影响LegacyASPNETSynchronizationContext)
深入研究本文,看看它是否与您的问题匹配,我相信您会在其中找到有用的信息。
还有另一种解决方法here,使用嵌套容器,您也可以检查它。