Runtime: CallContext.LogicalSetData does not maintain the value in the logical thread when used with Parallel.

Created on 5 Apr 2017  路  9Comments  路  Source: dotnet/runtime

CallContext.LogicalSetData is expected to maintain a certain value associated with the logical thread. This is not the case when used with Parallel.

Consider the code:

```c#
class Program
{
static void Main(string[] args)
{
CallContext.LogicalSetData("x-cor", "555");

        var list = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        //WithTasks(list);
        //WithParallel(list);

        Console.Read();
    }

    private static void WithParallel(IEnumerable<int> list)
    {
        Parallel.ForEach(list, TheTask);
    }

    private static void WithTasks(List<int> list)
    {
        var tasks = new Task[10];
        list.ForEach(i =>
        {
            tasks[i] = Task.Factory.StartNew(() =>
            {
                TheTask(i);
            });
        });

        Task.WaitAll(tasks);
    }

    private static void TheTask(int i)
    {
        if (i == 5)
        {
            CallContext.LogicalSetData("x-cor", "666");
        }

        Console.WriteLine($"thread {i} : {CallContext.LogicalGetData("x-cor")}");
    }
}
If you uncomment ```//WithTasks(list);```, the code outputs as expected (the value is maintained for each thread:

thread 0 : 555
thread 1 : 555
thread 2 : 555
thread 3 : 555
thread 4 : 555
thread 5 : 666
thread 6 : 555
thread 7 : 555
thread 8 : 555
thread 9 : 555

But when you uncomment ```//WithParallel(list);``` the code does not output the expected result:

thread 2 : 555
thread 4 : 555
thread 5 : 666
thread 6 : 666
thread 7 : 666
thread 8 : 666
thread 9 : 666
thread 0 : 555
thread 1 : 555
thread 3 : 555
```

The value 666 should only be for thread 5, but it was for multiple threads.

All 9 comments

Parallel does not make any guarantees about the order of processing of the input items, nor about what thread it uses to handle each item, nor about how it does or does not reset ExecutionContext between individual invocations. The output you shared is within the realm of a reasonable result for Parallel.

(For better or worse, CallContext also doesn't currently exist in corefx.)

@stephentoub Are you saying that while Task API maintains a separate execution context, Parallel API might not? Very strange.

Where can I find the .NET framework project page?

Are you saying that while Task API maintains a separate execution context, Parallel API might not?

A task is a single entity that executes one delegate, and ExecutionContext is flown to it. Parallel is a higher level helper that executes many delegates, and ExecutionContext is flown to it, such that state from out of the loop is available in the loop, just as with Task, but it's not guaranteed to be reset between iterations, just as if you wrote your own foreach in the body of a task it wouldn't implicitly reset EC between each iteration.

Very strange.

Why? If you really want the behavior of a single task per invocation, you can do exactly that. If you want the faster, less overhead semantics of Parallel.ForEach (setting and resetting EC between every iteration would have a very measurable negative impact), you can use it.

Where can I find the .NET framework project page?

You mean repo? The .NET Framework is not open source on GitHub. But you can find the reference source for it at http://referencesource.microsoft.com.

I'm going to close the issue. Feel free to re-open if you still believe there's an issue here to be addressed. Thanks.

But for me, Parallel.Foreach was intelligent task management considering the system resources. I now don't know exactly what other things are different in tasks vs Parallel.ForEach.

We have a similar behavior to Parallel in our nancy based http-server: when multiple requests arrive, call context (which is assigned at the beginning of each request) sometimes pops out in a completely different request. We noticed that it's taken from one of the previous requests completed about a second ago.
@stephentoub, Can this be the same issue?
As a fix, at the end of each request we call CallContext.FreeNamedDataSlot

Can this be the same issue?

It's hard to know without seeing the code in question. If you can come up with a small repro, please feel free to share it (in a new issue).

I can't reproduce the issue in a consistent way, but if I do it in future I will let you know in a separate thread. Thanks for your feedback.

Was this page helpful?
0 / 5 - 0 ratings