11

I have a web application and I make use of a lot of async operations using async/await. Everything worked fine, but when I created custom tasks to run multiple things in parallel, I noticed, that within this task the current culture changes after an await. The problem seems to be, that the threadpool uses the culture of the operation system, which is different from the culture of the request and that the default synchronization does not updates the culture, even when changing the culture of the current thread within the task.

So I create a custom synchronization context:

public sealed class CulturePreservingSynchronizationContext : SynchronizationContext
{
    private CultureInfo culture;
    private CultureInfo cultureUI;

    public CulturePreservingSynchronizationContext()
    {
        GetCulture();
    }

    public void MakeCurrent()
    {
        SetCulture();

        SynchronizationContext.SetSynchronizationContext(CreateCopy());
    }

    public override SynchronizationContext CreateCopy()
    {
        CulturePreservingSynchronizationContext clonedContext = new CulturePreservingSynchronizationContext();
        clonedContext.culture = culture;
        clonedContext.cultureUI = cultureUI;

        return clonedContext;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        base.Post(s =>
        {
            SetCulture();
            d(s);
        }, state);
    }

    public override void OperationStarted()
    {
        GetCulture();

        base.OperationStarted();
    }

    private void SetCulture()
    {
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = cultureUI;
    }

    private void GetCulture()
    {
        culture = CultureInfo.CurrentCulture;
        cultureUI = CultureInfo.CurrentUICulture;
    }
}

You can use it like this. In my simple example it works fine, but I have no real understanding of the very details to evaluate my approach (btw: my os-culture is de-DE). Please note that this is just an example and has nothing to do with the real code. I have just written this to demonstrate that the culture after the await is different to the culture before the await.

    Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

    CulturePreservingSyncContext context = new CulturePreservingSyncContext();

    Task.Run(async () =>
    {
        context.MakeCurrent();

        Console.WriteLine(CultureInfo.CurrentCulture);

        WebClient client = new WebClient();
        string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

        Console.WriteLine(CultureInfo.CurrentCulture);

    }).Wait();

Any advice is really welcome to understand if the implementation of the synchronization-context is good or not and if not, if there are any better solutions. I do not want to open a discussion if async and await or tasks are good or not in my situation.

SebastianStehle
  • 2,409
  • 1
  • 22
  • 32

3 Answers3

8

when I created custom tasks to run multiple things in parallel

It's important to distinguish between concurrent (doing multiple things at the same time) and parallel (using multiple threads to do multiple CPU-bound operations at the same time).

I noticed, that within this task the current culture changes after an await.

If you're doing parallel code (i.e., Parallel or Task.Run), then your operations should be CPU-bound, and they shouldn't contain an await at all.

It's a good idea to avoid parallel code on the server. Task.Run on ASP.NET is almost always a mistake.

Your example code is doing asynchronous work (DownloadStringTaskAsync) in a background thread (Task.Run) and synchronously blocking on it (Wait). That doesn't make any sense.

If you have asynchronous work to do, then you can just use async-ready primitives such as Task.WhenAll. The following code will preserve culture when run on ASP.NET, and will perform three downloads simultaneously:

private async Task TestCultureAsync()
{
  Debug.WriteLine(CultureInfo.CurrentCulture);

  WebClient client = new WebClient();
  string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

  Debug.WriteLine(CultureInfo.CurrentCulture);
}

var task1 = TestCultureAsync();
var task2 = TestCultureAsync();
var task3 = TestCultureAsync();
await Task.WhenAll(task1, task2, task3);
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • The example code has nothing to do with my web-application, it is a console application for demonstration purposes. I just added the information to my question. In my situation I run about 10 operations in parallel. They include IO and cpu bound operations like making web requests, processing results, writing results to a database (which is synchronous, because of a bad mongodb driver). So, in my opinion, it makes sense and performs quite well. The problem with the culture exists whenever you change the currentculture and then make a some asynchronous operations. – SebastianStehle Mar 08 '14 at 20:15
  • My recommendation is still the same. Use `Task.WhenAll` instead of multiple threads for anything I/O-bound. Do CPU-bound work synchronously and not in parallel. – Stephen Cleary Mar 08 '14 at 20:53
  • In general I totally agree, but it makes no sense to discuss this in the current context without having a look at the real code and the architecture. It is like a recommendation "Use properties instead of public fields". In most cases it is the better to use properties but there are situations where it makes a huge difference in performance when you use fields instead of properties (like math-calculations). But it was my mistake not to ask a specific question. I wanted to get some feedback one the synchronization-context. Sorry for that. Btw: You havee an interesting blog. – SebastianStehle Mar 08 '14 at 21:46
  • I know this is old, but the example code provided is not actually correct. the writelines are going to execute in the thread that called TestCultureAsync, then it will go to a different thread for the download, and come back to the original thread for the second writeline. If you wrap the whole thing with a return Task.Run, you would see the problem the original post described – Alpine Mar 31 '17 at 23:02
  • @Alpine: `TestCultureAsync` may or may not resume on the same thread. There is no "different thread" for the download. – Stephen Cleary Mar 31 '17 at 23:13
  • The first writeline is definitely on the caller's thread, and the way the code is written the awaited task doesn't have `ConfigureAwait(false)` on it meaning the synchronization context will be captured, and so the line after the await will be on the same thread as the caller as well. The await on the downloadStringTaskAsync indicates clearly that calling downloadStringTaskAsync will go off to a different thread to perform the download, and then set the result on the Task which will be unwrapped from the task and set to s. – Alpine Apr 03 '17 at 19:34
  • @Alpine: [The synchronization context of a web app does not necessarily resume on the same thread](https://msdn.microsoft.com/en-us/magazine/dn802603.aspx). `await` does not indicate anything about different threads, and [most I/O is not "performed" on a thread](http://blog.stephencleary.com/2013/11/there-is-no-thread.html). – Stephen Cleary Apr 04 '17 at 00:07
3

I can't believe the current culture isn't flowing as part of the ExecutionContext. That's a shame.

Recently I was browsing through the identity assemblies, when I noticed that all the async calls are using an extension which is implemented in Microsoft.AspNet.Identity.TaskExtensions, but it's internal. Copy & paste it from there using Resharper, IlSpy, etc. It's being used like this: await AsyncMethod().WithCurrentCulture().

It's the most complete implementation I've seen so far, and it's coming from a trusted source.

UPDATE: This problem has been fixed in .NET 4.6! The docs have been updated but also did a quick test to confirm this.

MoonStom
  • 2,847
  • 1
  • 26
  • 21
0

As pointed out by Stephen's answer, you shouldn't be using Task.Run while processing an ASP.NET request. It's almost always redundant, only adding a thread switch overhead and hurting the scalability of your web app. Even if you do a CPU-bound work, you should think twice before parallelizing it in ASP.NET context, as you'd reduce the number of concurrent client-side requests your web app could be able to process.

Otherwise, the Culture gets correctly flowed by AspNetSynchronizationContext. Besides, there's another problem with your code:

Task.Run(async () =>
{
    context.MakeCurrent();

    Console.WriteLine(CultureInfo.CurrentCulture);

    WebClient client = new WebClient();
    string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

    Console.WriteLine(CultureInfo.CurrentCulture);

}).Wait();

You install a custom synchronization context on a pool thread, but you don't remove it. Thus, the thread goes back to the pool with your context still being installed. This may bring some unpleasant surprises when this thread gets later re-used within the same ASP.NET process.

You might be better off with Stephen Toub's CultureAwaiter, which doesn't require installing a custom synchronization context.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • When I introduced the parallelization half a year ago, I was aware that it can cause problems with scalability, but so far it works very fine. I could improve the time to response (it is a scenario, where results are partially pushed to the client with signalR) and I have no scalability problems yet with about 3Mio requests per Month. I might remove the tasks at a later point, especially when the mongo-driver gets support for async-operations but at the moment it works very well. Thanks for pointing out that I should reset the synchronization context. – SebastianStehle Mar 09 '14 at 13:46
  • I have seen the solution with the awaiter and what I dont like is that I have to put it on each single await (potentially) and I have hundreds of them and also move the repsonsiblity to some classes which should not care about it. – SebastianStehle Mar 09 '14 at 13:48
  • @Noseration - I find it very weird that asp.net wont remove synchronization context when it is reused.What about security impersonation - would that also be reused? – Royi Namir Sep 26 '15 at 11:22
  • @RoyiNamir, ASP.NET actually installs and removes the s.context for each await continuation, when it enters and leaves the continuation thread. The impersonation is handled correctly, these's no security issue here) – noseratio Sep 26 '15 at 12:24
  • OK I might be missing something here. You said _the thread goes back to the pool with your context still being installed_ So I assumed that it is a bad thing ? I mean - looking at [this](http://i.imgur.com/WySdkkd.png) - when that thread's zone 1 is set with SC , and the thread is back for reuse - do the other code from different location which is glued to that reused thread - will have the same SC ??? – Royi Namir Sep 26 '15 at 12:36
  • @RoyiNamir, the OP doesn't mentioned ASP.NET anywhere in his question :) I assume it's a self-hosting Web API or a similar framework with its own custom s.context (note he's using `CulturePreservingSynchronizationContext`; there is no `AspNetSynchronizationContext` involved here). If he used ASP.NET, he wouldn't have an issue in the first place, as `AspNetSynchronizationContext` does correctly flow `CurrentCulture`. – noseratio Sep 26 '15 at 12:53
  • Or even worse, he might be overriding `AspNetSynchronizationContext` with his own `CulturePreservingSynchronizationContext`. In which case I'm note sure about the actual behavior. – noseratio Sep 26 '15 at 12:54
  • So let me get it straight , in asp.net when thread is going out for reuse , it's SC is reset . right ? only when it/another thread come back , the SC is restored. right ? I'm confused becuase Stephen says [**here**](http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx) that culture is not flowing right and it needs to be restored (look at the buttom example) ("_culture information is not flowed by default as part of ExecutionContext_") – Royi Namir Sep 26 '15 at 13:03
  • *it's SC is reset . right ? only when it/another thread come back , the SC is restored. right* - Right. It gets set [here](http://referencesource.microsoft.com/#System.Web/httpserverutility.cs,497), a new instance each time, then gets restored [here](http://referencesource.microsoft.com/#System.Web/httpserverutility.cs,649). I should have phrased my answer like "... This may bring some unpleasant surprises when this thread gets later re-used within the same web application process", as I believe he's self-hosting or using a 3rd party OWIN host. – noseratio Sep 26 '15 at 13:04
  • @RoyiNamir, Stephen Toub is correct in that `ExecutionContext` doesn't flow the culture (in .NET <= 4.5; I believe they've changed it in 4.6 where it does flow the culture). But `AspNetSynchronizationContext` flows much more of the state, including `CurrentCulture` and `HttpContext.Current`, on top of what `ExecutionContext` flows. Check [this](http://stackoverflow.com/q/17242094/1768303). – noseratio Sep 26 '15 at 13:11