【问题标题】:ASP.NET MVC 5 Synchronized HttpContext and Parallel.For(each) can it work?ASP.NET MVC 5 同步的 HttpContext 和 Parallel.For(each) 可以工作吗?
【发布时间】:2016-10-30 17:35:45
【问题描述】:

基本问题

在 ASP.NET MVC 5 下使用任务/线程时,HttpContext.Current 和/或其包含的实例变为空。这留下了例如Parallel Tasks 下的会话管理无用。我们将 User 实例存储在 Session 上。

经过大量阅读,我找到了一个适用于在普通循环中创建的任务和 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.For...:

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

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

问题

上述Tasks解决方案和Parallel.For有什么区别? Parallel.For 会阻塞 Request 线程吗?

欢迎帮助!

谢谢

编辑 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"];

在 Load with multiple Browsers 下使用插入 1000 个客户的 MVC 控制器请求对其进行了测试。插入一切正常。

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

感谢您的反馈!

编辑 2

最少、完整和可验证的示例:

Example 数据访问层示例:

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!");
    }
}

示例 Parallel.For 失败的测试。

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

示例 Parallel.For 测试有效,但似乎很慢(呃):

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

启动任务的普通 for 循环:失败

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.LogicalGetData 来研究这个。 第一次测试表明 ASP.NET MVC 5 下的并行任务比顺序任务快 3 倍。插入 1000 个客户。

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

新问题: 是否专门将用户实例添加到逻辑 CallContext 流线程安全中?所以每个请求线程都必须使用 LogicalSetData 并且它的子任务/线程使用 LogicalGetData。

再次感谢您的反馈!

干杯

【问题讨论】:

  • 如果 User 是您需要的上下文中唯一的数据,那么有更简单的方法可以传递它...

标签: c# asp.net-mvc multithreading parallel-processing


【解决方案1】:

Parallel.For 会阻塞 Request 线程吗?

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

【讨论】:

  • 但是使用任务解决方案它使用等待,然后发送不会阻塞!所以任务中的等待似乎与并行中的行为不同。对于...?
  • 更加陌生,因为您使用 RunSynchronously 执行它们。没有周围的代码,我无法进一步解决这个问题。考虑创建一个minimal reproducible example
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 2011-10-22
    • 2022-12-09
    • 2014-05-06
    • 1970-01-01
    • 2012-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-05
    相关资源
    最近更新 更多