与 MS AsyncHelper 同步的空异常 运行 异步方法
Null exception running async method synchronously with MS AsyncHelper
由于您不能 运行 来自子 window (@Html.Action
) 调用的异步方法,我一直在寻找 运行 的最简单方法来自 非异步 方法的异步 任务。这将允许我的 MainMenu
控制器 Menu
操作在像这样注入时仍然有效(而不是必须移动到 VM 或 Ajax 解决方案):
<div class="row">
@Html.Action("MainMenu", "Menu")
</div>
我使用 MS 自己使用的代码副本尝试了这种有前途的方法:How to call asynchronous method from synchronous method in C#?
AsyncHelper 代码:
public static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return AsyncHelper._myTaskFactory
.StartNew<Task<TResult>>(func)
.Unwrap<TResult>()
.GetAwaiter()
.GetResult();
}
public static void RunSync(Func<Task> func)
{
AsyncHelper._myTaskFactory
.StartNew<Task>(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
我是这样吃的:
public async Task<ActionResult> MainMenu()
{
if (_currentCandidate == null)
{
throw new ArgumentNullException("_currentCandidate");
}
var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());
[snip]
}
哪个调用这个异步方法:
public async Task<int> CandidateIdAsync()
{
var applicationUser = await this.ApplicationUserAsync();
if (applicationUser != null)
{
return applicationUser.CandidateId.GetValueOrDefault();
}
return 0;
}
只有当我 运行 时,我才会收到以下错误:
我在这里错过了什么?该代码看起来应该可以工作,但我对它还不够熟悉,无法弄清楚。
更新:
作为参考,MainMenu
控制器 class 如下所示:
public class MenuController : Controller
{
readonly ICurrentCandidate _currentCandidate;
public MenuController(ICurrentCandidate currentCandidate)
{
_currentCandidate = currentCandidate;
}
// GET: MainMenu
public ActionResult MainMenu()
{
if (_currentCandidate == null)
{
throw new ArgumentNullException("_currentCandidate");
}
var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());
[snip]
return View(vm);
}
}
另一个更新:
失败似乎是在相关的 IF 代码中作为简化的 CandidateIdAsnyc
工作:
// This works
public async Task<int> CandidateIdAsync()
{
return 0;
}
这是该代码的其余部分:
public class CurrentCandidate : ICurrentCandidate
{
private readonly ApplicationDbContext _applicationDbContext;
private readonly IApplicationUserManager _userManager;
private readonly ICandidateStore _candidateStore;
public CurrentCandidate(ApplicationDbContext applicationDbContext, ICandidateStore candidateStore, IApplicationUserManager userManager)
{
this._candidateStore = candidateStore;
this._applicationDbContext = applicationDbContext;
this._userManager = userManager; // new ApplicationUserManager(new UserStore<ApplicationUser>(this._applicationDbContext));
}
public async Task<ApplicationUser> ApplicationUserAsync()
{
var applicationUser = await this._userManager.FindByIdAsync(HttpContext.Current.User.Identity.GetUserId());
return applicationUser;
}
public bool IsAuthenticated()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
public async Task<int> CandidateIdAsync()
{
var applicationUser = await this.ApplicationUserAsync();
if (applicationUser != null)
{
return applicationUser.CandidateId.GetValueOrDefault();
}
return 0;
}
}
运行同步异步方法的标准方法是
var r = Task.Run( () => MyAsynchMethod(args)).Result;
或
var task = MyAsynchMethod(args);
task.ConfigureAwait(false);
var r = task.Result;
两者都不会导致死锁,因为它们 运行 至少是方法的异步 return 部分,没有 ASP.Net 同步上下文。
现在大多数与 ASP.Net 相关的方法(如您的情况下的呈现或标识)都期望 HttpContext.Current
被正确设置为 "current" 上下文 - 这显然不是代码时的情况在没有 "entered" ASP.Net 同步上下文的线程上结束 运行ning(你也失去了当前的文化,但至少不会导致 NRE)。
肮脏的解决方法 - 手动设置上下文,但是你 运行 有从多个线程并行访问相同上下文的危险 - 如果小心使用它可能没问题(即如果 "main" 请求线程正在等待在 .Result
上其他线程可以使用它):
var context = HttpContext.Current;
var r = Task.Run( () =>
{
HttpContext.Current = context;
return MyAsynchMethod(args);
}
).Result;
请注意,您最好恢复上下文和 set/restore 两者 CurrentCulture
/CurrentUICulture
。
I have been searching for the simplest way to run async tasks from a non-async method.
这个问题已经讨论过很多次了,没有适用于所有场景的解决方案。内部 AsyncHelper
类型仅在 ASP.NET 团队知道它是安全的非常特殊的情况下使用;这不是通用解决方案。
一般的做法是:
- 阻止(使用
Result
或 GetAwaiter().GetResult()
)。这种方法 can cause deadlocks(如我在我的博客中所述)除非您始终使用 ConfigureAwait(false)
- 并且您调用的所有代码也始终使用 ConfigureAwait(false)
。但是,请注意您的代码 不能 使用 ConfigureAwait(false)
除非它实际上不需要 ASP.NET 上下文。
- 嵌套消息循环。这可以使用我的 AsyncEx 库中的
AsyncContext
之类的东西。但是,有很多 ASP.NET API 隐含地假定当前 SynchronizationContext
是 AspNetSynchronizationContext
,而 AsyncContext
. 中的情况并非如此。
- 用阻塞分离线程(使用
Task.Run(...).GetAwaiter().GetResult()
)。这种方法避免了您可以通过阻塞看到的死锁,但它确实在 ASP.NET 上下文之外执行代码。这种方法也会对您的可扩展性产生负面影响(这是首先在 ASP.NET 上使用 async
的全部意义)。
换句话说,这些只是技巧。
ASP.NET vNext 有"view components" 的概念,可能是async
,所以这在未来是一个自然的解决方案。对于今天的代码,IMO 最好的解决方案是使方法同步;这比实施 hack 更好。
谢谢大家的指教。
"workarounds" 的 None 有效,我们需要保留现有的 async
代码,所以我决定不反对 async
.
我完全不使用 @Html.Action
避免了这个问题。
相反,我使用菜单作为部分视图使用
@Html.Partial("MainMenu", @ViewBag.MainMenuViewModel);
并在我们的基本控制器中设置该模型。
由于您不能 运行 来自子 window (@Html.Action
) 调用的异步方法,我一直在寻找 运行 的最简单方法来自 非异步 方法的异步 任务。这将允许我的 MainMenu
控制器 Menu
操作在像这样注入时仍然有效(而不是必须移动到 VM 或 Ajax 解决方案):
<div class="row">
@Html.Action("MainMenu", "Menu")
</div>
我使用 MS 自己使用的代码副本尝试了这种有前途的方法:How to call asynchronous method from synchronous method in C#?
AsyncHelper 代码:
public static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return AsyncHelper._myTaskFactory
.StartNew<Task<TResult>>(func)
.Unwrap<TResult>()
.GetAwaiter()
.GetResult();
}
public static void RunSync(Func<Task> func)
{
AsyncHelper._myTaskFactory
.StartNew<Task>(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
我是这样吃的:
public async Task<ActionResult> MainMenu()
{
if (_currentCandidate == null)
{
throw new ArgumentNullException("_currentCandidate");
}
var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());
[snip]
}
哪个调用这个异步方法:
public async Task<int> CandidateIdAsync()
{
var applicationUser = await this.ApplicationUserAsync();
if (applicationUser != null)
{
return applicationUser.CandidateId.GetValueOrDefault();
}
return 0;
}
只有当我 运行 时,我才会收到以下错误:
我在这里错过了什么?该代码看起来应该可以工作,但我对它还不够熟悉,无法弄清楚。
更新:
作为参考,MainMenu
控制器 class 如下所示:
public class MenuController : Controller
{
readonly ICurrentCandidate _currentCandidate;
public MenuController(ICurrentCandidate currentCandidate)
{
_currentCandidate = currentCandidate;
}
// GET: MainMenu
public ActionResult MainMenu()
{
if (_currentCandidate == null)
{
throw new ArgumentNullException("_currentCandidate");
}
var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());
[snip]
return View(vm);
}
}
另一个更新:
失败似乎是在相关的 IF 代码中作为简化的 CandidateIdAsnyc
工作:
// This works
public async Task<int> CandidateIdAsync()
{
return 0;
}
这是该代码的其余部分:
public class CurrentCandidate : ICurrentCandidate
{
private readonly ApplicationDbContext _applicationDbContext;
private readonly IApplicationUserManager _userManager;
private readonly ICandidateStore _candidateStore;
public CurrentCandidate(ApplicationDbContext applicationDbContext, ICandidateStore candidateStore, IApplicationUserManager userManager)
{
this._candidateStore = candidateStore;
this._applicationDbContext = applicationDbContext;
this._userManager = userManager; // new ApplicationUserManager(new UserStore<ApplicationUser>(this._applicationDbContext));
}
public async Task<ApplicationUser> ApplicationUserAsync()
{
var applicationUser = await this._userManager.FindByIdAsync(HttpContext.Current.User.Identity.GetUserId());
return applicationUser;
}
public bool IsAuthenticated()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
public async Task<int> CandidateIdAsync()
{
var applicationUser = await this.ApplicationUserAsync();
if (applicationUser != null)
{
return applicationUser.CandidateId.GetValueOrDefault();
}
return 0;
}
}
运行同步异步方法的标准方法是
var r = Task.Run( () => MyAsynchMethod(args)).Result;
或
var task = MyAsynchMethod(args);
task.ConfigureAwait(false);
var r = task.Result;
两者都不会导致死锁,因为它们 运行 至少是方法的异步 return 部分,没有 ASP.Net 同步上下文。
现在大多数与 ASP.Net 相关的方法(如您的情况下的呈现或标识)都期望 HttpContext.Current
被正确设置为 "current" 上下文 - 这显然不是代码时的情况在没有 "entered" ASP.Net 同步上下文的线程上结束 运行ning(你也失去了当前的文化,但至少不会导致 NRE)。
肮脏的解决方法 - 手动设置上下文,但是你 运行 有从多个线程并行访问相同上下文的危险 - 如果小心使用它可能没问题(即如果 "main" 请求线程正在等待在 .Result
上其他线程可以使用它):
var context = HttpContext.Current;
var r = Task.Run( () =>
{
HttpContext.Current = context;
return MyAsynchMethod(args);
}
).Result;
请注意,您最好恢复上下文和 set/restore 两者 CurrentCulture
/CurrentUICulture
。
I have been searching for the simplest way to run async tasks from a non-async method.
这个问题已经讨论过很多次了,没有适用于所有场景的解决方案。内部 AsyncHelper
类型仅在 ASP.NET 团队知道它是安全的非常特殊的情况下使用;这不是通用解决方案。
一般的做法是:
- 阻止(使用
Result
或GetAwaiter().GetResult()
)。这种方法 can cause deadlocks(如我在我的博客中所述)除非您始终使用ConfigureAwait(false)
- 并且您调用的所有代码也始终使用ConfigureAwait(false)
。但是,请注意您的代码 不能 使用ConfigureAwait(false)
除非它实际上不需要 ASP.NET 上下文。 - 嵌套消息循环。这可以使用我的 AsyncEx 库中的
AsyncContext
之类的东西。但是,有很多 ASP.NET API 隐含地假定当前SynchronizationContext
是AspNetSynchronizationContext
,而AsyncContext
. 中的情况并非如此。
- 用阻塞分离线程(使用
Task.Run(...).GetAwaiter().GetResult()
)。这种方法避免了您可以通过阻塞看到的死锁,但它确实在 ASP.NET 上下文之外执行代码。这种方法也会对您的可扩展性产生负面影响(这是首先在 ASP.NET 上使用async
的全部意义)。
换句话说,这些只是技巧。
ASP.NET vNext 有"view components" 的概念,可能是async
,所以这在未来是一个自然的解决方案。对于今天的代码,IMO 最好的解决方案是使方法同步;这比实施 hack 更好。
谢谢大家的指教。
"workarounds" 的None 有效,我们需要保留现有的 async
代码,所以我决定不反对 async
.
我完全不使用 @Html.Action
避免了这个问题。
相反,我使用菜单作为部分视图使用
@Html.Partial("MainMenu", @ViewBag.MainMenuViewModel);
并在我们的基本控制器中设置该模型。