The Microsoft documentation about AsyncLocal states that:
Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.
I have a class that is used to capture some data during code execution and may be used in async code. I was trying to use an AsyncLocal to share data in async flows and it works "as expected" when using tasks.
However, it is a bit strange when doing a parallel.for.
Example:
var asyncValue = new AsyncLocal<int>();
Parallel.For(1, 30, _ =>
{
asyncValue.Value = asyncValue.Value + 1;
Console.Write($"{asyncValue.Value}, ");
});
I was expecting the ouput to be 1 for all executions, but it isn't.
Output example:
1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2,
Then, by adding the Task.CurrentId to the output, I can see that the values start to be different than 1 when the task ID is "reused".
$"{Task.CurrentId} - {asyncValue.Value}"
14 - 1
8 - 1
9 - 1
9 - 2
22 - 1
23 - 1
14 - 2
8 - 2
...
It looks like tasks are being reused to run more than one execution (probably because the loop has more iterations that the threads available) and the AsyncValue capture by the first task execution is also being shared.
Is this the expected behavior?
I need to share data between tasks and their possible child tasks, but I have no control on how those tasks are created (as they are created by external code using my library). Can't use AsyncLocal because I would never know if the data is being share with descendant or sibling tasks.
Update 1: Some context:
- The class that is part of a library that is used by other developers.
- The main goal is to keep tracking of changes done to some objects (something like a database log)
- The changes are organized in a three, like the call stack. So we can see what have changed in which method, what are the parent and child methods, order, etc.
- What I really need is a way to share data between
tasksand its childreentasks. - One option is to provide a method that external code must use to create
tasks/performparallel.for. This way I can share data using parameters/returns/captured variables, etc. - But,
AsyncLocalwould be much cleaner, if the data wasn't shared between siblingtasksonparallel.for. - Note that currently I do not control how the external code use my library, so they can use
tasks/parallel.for/etc, as they need.
Update 2:
If we declare theparallel.for action as async, it no longer shares the AsyncLocal data, even though it still reuses the task IDs.
var asyncValue = new AsyncLocal<int>();
Parallel.For(1, 30, **async** _ =>
{
asyncValue.Value = asyncValue.Value + 1;
Console.Write($"{asyncValue.Value}, ");
});
$"{Task.CurrentId} - {asyncValue.Value}"
12 - 1
8 - 1
9 - 1
10 - 1
11 - 1
13 - 1
13 - 1
13 - 1
13 - 1
13 - 1
13 - 1
13 - 1
...
AsyncLocalis an object that allows storing data local to async flow. This of course doesn't happen automagically, for that to work something has to happen in the background. Just like for ThreadLocal to work something has to happen in background: os has to attach data to current thread, and so some ambient context of thread has to exist. It is just a detail hidden from us by the os. This ambient context attached to thread can be accessed in C# through ExecutionContext, read more about it in the excelent Stephen Toub's article here: https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/So, the problem is that
Parallelmethods doesn't seem to capture the ExecutionContext you're currently running on. This can be fixed like that:and this is pretty much what the language does for you when you use higher level constructs like Tasks and async/await.
That being said, I strongly suggest you pass the context explicitly, not through ambient constructs like ExecutionContext, which is hard to test and maintain.