与MS AsyncHelper同步运行异步方法的空异常

时间:2015-02-23 16:58:29

标签: c# asp.net-mvc razor asynchronous synchronous

由于您无法从子窗口(@Html.Action)调用运行异步方法,因此我一直在寻找从非异步运行 async 任务的最简单方法方法。这将允许我的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;
    }

只有在我运行时才会出现以下错误:

enter image description here

我在这里缺少什么?代码看起来应该可以工作,但我还不熟悉它还没弄明白。

更新:

作为参考,MainMenu控制器类如下所示:

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;
    }
}

3 个答案:

答案 0 :(得分:4)

  

我一直在寻找从非异步方法运行异步任务的最简单方法。

这已经多次讨论过,并且没有适用于所有情况的解决方案。内部AsyncHelper类型仅在ASP.NET团队知道其安全的非常特定的情况下使用;它不是一个通用的解决方案。

通常,方法是:

  1. 阻止(使用ResultGetAwaiter().GetResult())。这种方法can cause deadlocks(正如我在博客中描述的那样),除非您始终使用ConfigureAwait(false) - 并且您调用的所有代码也始终使用ConfigureAwait(false)。但请注意,您的代码不能使用ConfigureAwait(false),除非它实际上并不需要ASP.NET上下文。
  2. 嵌套消息循环。这可以使用我的AsyncEx库中的AsyncContext之类的东西。但是,有很多ASP.NET API隐含地假设当前SynchronizationContextAspNetSynchronizationContextAsyncContext中并非如此。
  3. 使用阻止分隔线程(使用Task.Run(...).GetAwaiter().GetResult())。这种方法避免了只能阻塞的死锁,但它确实执行了ASP.NET上下文之外的代码。这种方法也会对您的可伸缩性产生负面影响(这首先是在ASP.NET上使用async的全部内容)。
  4. 换句话说,这些只是黑客攻击。

    ASP.NET vNext具有&#34;查看组件&#34;的概念。这可能是async,所以这将是未来的自然解决方案。对于今天的代码,IMO最好的解决方案是使方法同步;这比实施黑客更好。

答案 1 :(得分:1)

感谢大家的建议。

没有&#34;变通办法&#34;工作,我们需要保留现有的async代码,因此我决定不与async作对。

我完全不使用@Html.Action来避免这个问题。

相反,我使用

将菜单用作局部视图
@Html.Partial("MainMenu", @ViewBag.MainMenuViewModel);

并在我们的基本控制器中设置该模型。

答案 2 :(得分:-1)

同步运行异步方法的标准方法是

var r = Task.Run( () => MyAsynchMethod(args)).Result;

var task = MyAsynchMethod(args);
task.ConfigureAwait(false);
var r = task.Result;

两者都不会导致死锁,因为如果没有ASP.Net Synchronization上下文,它们至少会运行方法的异步返回部分。

现在,大多数与ASP.Net相关的方法(如你的情况下的渲染或身份)都希望HttpContext.Current正确设置为&#34;当前&#34;上下文 - 当代码最终在没有&#34;输入&#34;的线程上运行时,显然不是这种情况。 ASP.Net同步上下文(你也放松了当前的文化,但至少它不会导致NRE)。

脏的解决方法 - 手动设置上下文,但是你会遇到从多个线程并行访问相同上下文的危险 - 如果小心使用它可能没问题(即如果&#34;主要&#34;请求线程正在等待{{ 1}}其他线程可以使用它):

.Result

请注意,您最好还原上下文并设置/恢复 var context = HttpContext.Current; var r = Task.Run( () => { HttpContext.Current = context; return MyAsynchMethod(args); } ).Result; / CurrentCulture