1

Basic problem

When using Tasks/Threads under ASP.NET MVC 5 the HttpContext.Current and/or its contained instances become null. This leaves e.g. Session management useless under Parallel Tasks. And we store our User instance on the Session.

After lots of reading I found a solution that works with Tasks that are created in a plain loop and RunSynchronously. But for unknown reasons Parallel.For gets stuck in what looks a deadlock.

Current solution

My current solution is based on SynchronizationContext.Current being set for the Request Thread and NOT set for its "child" Tasks/Threads. On the Request thread I put the current SynchronizationContext into CallContext.LogicalSetData, Start all Tasks:

        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());

The magic comes with using the Send method on the stored SynchronizationContext: It runs the code/action in the original Request thread. Just like UI Threads doing it in WinForms:

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

Final problem

I've tested above solution and it works for both synchroon and async (await) Tasks. But not for Parallel.For...:

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

In the debugger all tasks/threads get stuck in the .Send method.

Questions

What is the difference between above Tasks solution and Parallel.For? Does the Parallel.For blocks the Request thread?

Help is welcome!

Thanks

Edit 1

Stumbled upon what looks like a solution:

        ParallelOptions pOptions = new ParallelOptions
        {
            TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
        };

        Parallel.For(0, 1000, pOptions, (idx) =>
        {
        ...

The SynchronizationContext and CallContext are not needed anymore.

Together with our IoC Container Unity its Register:

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

and its Resolve:

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

Tested it under Load with multiple Browsers with a MVC Controller request that inserts 1000 Customers. Al inserts went ok.

Can anybody tell me if this is the prefered way to go? I know that (Long running) tasks under ASP.NET (MVC) is not really a good thing but I wanted to know if its possible and maybe use it to speed up a few actions.

Thanks for feedback!

Edit 2

Minimal, Complete and Verifyable example(s):

Example Data access layer 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!");
    }
}

Example Parallel.For test that fails.

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

Example Parallel.For test that works, but seems to be slow(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");
}

Plain for loop that starts Tasks: Fails

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

Same plain loop but now starting Tasks with .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");
    }

As Henk Holterman pointed out there are other ways to get the User instance without having to provide a context variable in the whole flow down. We are now looking at this using CallContext.LogicalSetData / CallContext.LogicalGetData. First test showed that Parallel Tasks under ASP.NET MVC 5 are 3 times faster than Sequential. Inserting 1000 x one Customer.

Reading on internet showed that CallContext.Logical... are only safe in .NET 4.5+ and are not very well documented (At least not in MSDN)

New Question: Is specifically adding User instance into Logical CallContext flow threadsafe? So each request thread has to use LogicalSetData ands its child tasks/threads use LogicalGetData.

Again thanks for feedback!

Cheers

1
  • 1
    If User is the only piece of data you need from the context then there are easier methods to pass it along... Commented Oct 30, 2016 at 19:45

2 Answers 2

1

Does the Parallel.For blocks the Request thread?

When you call it from that thread, then: Yes.

Sign up to request clarification or add additional context in comments.

2 Comments

But with the task solution it use a waitall and then the send does not block! So the wait in task seems to behave different than in parallel.for...?
Even stranger because you execute them with RunSynchronously. Without the surrounding code I can't figure this out any further. Consider creating a minimal reproducible example
0

I think AsyncLocal will be a better option, it will copy the value to all child threads/tasks, but when you change it in any child thread, the value will NOT be visible by other threads, unless the value you saved is a object and you only change the value of that object, then it will be visible to other threads.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.