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