I'm struggling to find simple docs of what AsyncLocal<T>
does.
I've written some tests which I think tell me that the answer is "yes", but it would great if someone could confirm that! (especially since I don't know how to write tests that would have definitive control of the threads and continuation contexts ... so it's possible that they only work coincidentally!)
As I understand it,
ThreadLocal
will guarantee that if you're on a different thread, then you'll get a different instance of an object.- If you're creating and ending threads, then you might end up re-using the thread again later (and thus arriving on a thread where "that thread's"
ThreadLocal
object has already been used a bit). - But the interaction with
await
is less pleasant. The thread that you continue on (even if.ConfigureAwait(true)
) is not guaranteed to be the same thread you started on, thus you may not get the same object back out ofThreadLocal
on the otherside.
- If you're creating and ending threads, then you might end up re-using the thread again later (and thus arriving on a thread where "that thread's"
Conversely,
AsyncLocal
does guarantee that you'll get the same object either side of anawait
call.
But I can't find anywhere that actually says that AsyncLocal
will get a value that's specific to the initial thread, in the first place!
i.e.:
- Suppose you have an instance method (
MyAsyncMethod
) that references a 'shared'AsyncLocal
field (myAsyncLocal
) from its class, on either side of anawait
call. - And suppose that you take an instance of that class and call that method in parallel a bunch of times. * And suppose finally that each invocation happens to end up scheduled on a distinct thread.
I know that for each separate invocation of MyAsyncMethod
, myAsyncLocal.Value
will return the same object before and after the await (assuming that nothing reassigns it)
But is it guaranteed that each of the invocations will be looking at different objects in the first place?
As mentioned at the start, I've created a test to try to determine this myself. The following test passes consistently
public class AssessBehaviourOfAsyncLocal
{
private class StringHolder
{
public string HeldString { get; set; }
}
[Test, Repeat(10)]
public void RunInParallel()
{
var reps = Enumerable.Range(1, 100).ToArray();
Parallel.ForEach(reps, index =>
{
var val = "Value " + index;
Assert.AreNotEqual(val, asyncLocalString.Value?.HeldString);
if (asyncLocalString.Value == null)
{
asyncLocalString.Value = new StringHolder();
}
asyncLocalString.Value.HeldString = val;
ExamineValuesOfLocalObjectsEitherSideOfAwait(val).Wait();
});
}
static readonly AsyncLocal<StringHolder> asyncLocalString = new AsyncLocal<StringHolder>();
static async Task ExamineValuesOfLocalObjectsEitherSideOfAwait(string expectedValue)
{
Assert.AreEqual(expectedValue, asyncLocalString.Value.HeldString);
await Task.Delay(100);
Assert.AreEqual(expectedValue, asyncLocalString.Value.HeldString);
}
}