ASP.NET MVC 5同步HttpContext和Parallel.对于(每个)都可以工作

本文关键字:对于 每个 工作 都可以 Parallel MVC NET 同步 HttpContext ASP | 更新日期: 2023-09-27 18:15:31

基本问题

在ASP.NET MVC 5下使用Tasks/Threads时,HttpContext.Current和/或其包含的实例将变为null。这使得会话管理在并行任务下变得毫无用处。我们将用户实例存储在会话中。

经过大量阅读,我找到了一个解决方案,它可以与在纯循环和RunSynchronously中创建的任务一起使用。但由于未知的原因,Parallel.for陷入了看似僵局的境地。

当前解决方案

我目前的解决方案是基于SynchronizationContext.current为请求线程设置,而不是为其"子"任务/线程设置。在请求线程上,我将当前SynchronizationContext放入CallContext.LogicalSetData,启动所有任务:

        CallContext.LogicalSetData("HttpRequestSyncContext", SynchronizationContext.Current);

        List<Task> tasks = new List<Task>();
        for (int lc = 0; lc < 1000; lc++)
        {
            tasks.Add(new Task(() =>
            {
                /// Call ServiceLayer/DAL which needs Session["MyUser"]...
            }, CancellationToken.None, TaskCreationOptions.LongRunning));
        }
        tasks.ForEach(t => t.RunSynchronously());
        Task.WaitAll(tasks.ToArray());

神奇之处在于在存储的SynchronizationContext上使用Send方法:它在原始请求线程中运行代码/操作。就像UI线程在WinForms:中一样

        User myUser = null;
        SynchronizationContext requestSyncContext = (SynchronizationContext)CallContext.LogicalGetData("requestSyncContext");
        if (requestSyncContext != null)
        {
            requestSyncContext.Send( (state) =>
              {
                  myUser = (User)HttpContext.Current.Session["MyUser"];
              }, null);
        }

最终问题

我已经测试了上面的解决方案,它适用于同步和异步(等待(任务。但不适用于Parallel。适用于…:

        Parallel.For(0, 1000, (idx) =>
        {
           /// Call ServiceLayer/DAL which needs Session["MyUser"]...
        });

在调试器中,所有任务/线程都被困在.Send方法中。

问题

上面的任务解决方案和Parallel.For有什么区别?Parallel.For会阻塞请求线程吗?

欢迎帮助!

感谢

编辑1

偶然发现了一个看起来像是解决方案的东西:

        ParallelOptions pOptions = new ParallelOptions
        {
            TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
        };
        Parallel.For(0, 1000, pOptions, (idx) =>
        {
        ...

不再需要SynchronizationContext和CallContext。

与我们的IoC Container Unity一起注册:

       container.RegisterType<HttpContextBase>(
            new PerRequestLifetimeManager(),
            new InjectionFactory(x => { return new HttpContextWrapper(HttpContext.Current); })
        );

及其解决方案:

     HttpContextBase httpCtx = ServiceLayer.Container.Resolve<HttpContextBase>();
        return (User)httpCtx.Session["MyUser"];

通过插入1000个客户的MVC控制器请求,在多个浏览器的负载下对其进行了测试。Al插件正常。

有人能告诉我这是不是最好的方式吗?我知道在ASP.NET(MVC(下(长时间运行(任务并不是一件好事,但我想知道这是否可能,也许可以用来加快一些操作。

感谢您的反馈!

编辑2

最小、完整且可验证的示例:

示例数据访问层示例:

public void NeedsPresentationLayerUser()
{
    // Some work e.g. DB calls
    // Need User from Presentation layer
    HttpContextBase httpCtx = ServiceLayer.Container.Resolve<HttpContextBase>();
    string userName = (string)httpCtx.Session["MyUser"];
    if ( !userName.Equals("Me") )
    {
        throw new ApplicationException("Assert: UserName test failed!");
    }
}

并行示例。用于失败的测试。

public ActionResult MCVParallelTestFail()
{
    Session["MyUser"] = "Me";
    Parallel.For(0, 1000, (idx) =>
    {
        // Call down into Data Access layer...
        ServiceLayer.Db.SystemFactory.NeedsPresentationLayerUser();
    });
    return RedirectToAction("Index", "Home");
}

示例并行。对于有效但似乎很慢的测试(er(:

public ActionResult MCVParallelTestWorks()
{
    Session["MyUser"] = "Me";
    ParallelOptions pOptions = new ParallelOptions
    {
        TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
    };
    Parallel.For(0, 1000, pOptions, (idx) =>
    {
        // Call down into Data Access layer...
        ServiceLayer.Db.SystemFactory.NeedsPresentationLayerUser();
    });
    return RedirectToAction("Index", "Home");
}

启动任务的循环:失败

public ActionResult MCVTasksTestFail()
{
    Session["MyUser"] = "Me";
    for(int lc = 0; lc < 1000;  lc++ )
    {
        Task.Factory.StartNew(() =>
        {
            // Call down into Data Access layer...
            ServiceLayer.Db.SystemFactory.NeedsPresentationLayerUser();
        });
    }
    return RedirectToAction("Index", "Home");
}

相同的纯循环,但现在使用.RunSynchronously((启动任务:

public ActionResult MCVTasksTestWorks()
    {
        Session["MyUser"] = "Me";
        List<Task> tasks = new List<Task>();
        for(int lc = 0; lc < 1000;  lc++ )
        {
            tasks.Add(new Task(() =>
            {
                // Call down into Data Access layer...
                ServiceLayer.Db.SystemFactory.NeedsPresentationLayerUser();
            }));
        }
        tasks.ForEach(t => t.RunSynchronously());
        Task.WaitAll(tasks.ToArray());
        return RedirectToAction("Index", "Home");
    }

正如Henk Holterman所指出的,还有其他方法可以获得User实例,而不必在整个流程中提供上下文变量。我们现在使用CallContext.LogicalSetData/CallContext.LLogicalGetData来研究这个问题。第一次测试表明,ASP.NET MVC 5下的并行任务比Sequential快3倍。插入1000 x 1个客户。

互联网上的阅读表明,CallContext.Logical…仅在.NET 4.5+中是安全的,并且没有很好的文档记录(至少在MSDN中没有(

新问题:专门将用户实例添加到逻辑调用上下文流中是线程安全的吗?因此,每个请求线程都必须使用LogicalSetData,其子任务/线程使用LogicalGetData。

再次感谢您的反馈!

干杯

ASP.NET MVC 5同步HttpContext和Parallel.对于(每个)都可以工作

Parallel.For会阻塞请求线程吗?

当你从那个线程调用它时,那么:是的。

我认为AsyncLocal将是一个更好的选择,它将把值复制到所有子线程/任务中,但当你在任何子线程中更改它时,其他线程都看不到该值,除非你保存的值是一个对象,并且你只更改了该对象的值,否则它对其他线程都是可见的。