`AsyncLocal` 是否也做 `ThreadLocal` 做的事情?

Does `AsyncLocal` also do the things that `ThreadLocal` does?

我正在努力寻找有关 AsyncLocal<T> 功能的简单文档。

我写了一些测试,我 认为 告诉我答案是“是”,但如果有人能证实这一点,那就太好了! (特别是因为我不知道如何编写可以明确控制线程和延续上下文的测试......所以它们可能只是巧合!)


但是我找不到任何地方说 AsyncLocal 首先会得到一个特定于初始线程的值!

即:

我知道对于MyAsyncMethod的每个单独调用myAsyncLocal.Value将return等待之前和之后的相同对象(假设没有重新分配它)

但是是否可以保证每次调用首先会查看不同的对象?


如开头所述,我创建了一个测试来尝试自己确定这一点。以下测试一致通过

    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);
        }
    }

But is it guaranteed that each of the invocations will be looking at different objects in the first place?

没有。从逻辑上将其视为传递给函数的参数(不是 refout)。对象的任何更改(例如设置属性)都将被调用者看到。但是,如果您分配一个新值 - 它 不会被调用者 看到。

因此在您的代码示例中有:

Context for the test
 -> Context for each of the parallel foreach invocations (some may be "shared" between invocations since parallel will likely reuse threads)
   -> Context for the ExamineValuesOfLocalObjectsEitherSideOfAwait invocation

我不确定 context 是否正确 - 但希望你的想法是正确的。

因此 asynclocal 将从测试上下文(就像函数的参数一样)流向每个并行 foreach 调用等的上下文。这与 ThreadLocal 不同(它赢得了像那样流下去)。

以您的示例为基础,尝试一下:

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace NUnitTestProject1
{
    public class AssessBehaviourOfAsyncLocal
    {
        public class Tester
        {
            public int Value { get; set; }
        }

        [Test, Repeat(50)]
        public void RunInParallel()
        {
            var newObject = new object();
            var reps = Enumerable.Range(1, 5);
            Parallel.ForEach(reps, index =>
            {
                //Thread.Sleep(index * 50); (with or without this line, 
                Assert.AreEqual(null, asyncLocalString.Value);
                asyncLocalObject.Value = newObject;
                asyncLocalTester.Value = new Tester() { Value = 1 };

                var backgroundTask = new Task(() => {
                    Assert.AreEqual(null, asyncLocalString.Value);
                    Assert.AreEqual(newObject, asyncLocalObject.Value);
                    asyncLocalString.Value = "Bobby";
                    asyncLocalObject.Value = "Hello";
                    asyncLocalTester.Value.Value = 4;

                    Assert.AreEqual("Bobby", asyncLocalString.Value);
                    Assert.AreNotEqual(newObject, asyncLocalObject.Value);
                });

                var val = "Value " + index;
                asyncLocalString.Value = val;
                Assert.AreEqual(newObject, asyncLocalObject.Value);
                Assert.AreEqual(1, asyncLocalTester.Value.Value);

                backgroundTask.Start();
                backgroundTask.Wait();
                // Note that the Bobby is not visible here
                Assert.AreEqual(val, asyncLocalString.Value);
                Assert.AreEqual(newObject, asyncLocalObject.Value);
                Assert.AreEqual(4, asyncLocalTester.Value.Value);

                ExamineValuesOfLocalObjectsEitherSideOfAwait(val).Wait();
            });
        }

        static readonly AsyncLocal<string> asyncLocalString = new AsyncLocal<string>();
        static readonly AsyncLocal<object> asyncLocalObject = new AsyncLocal<object>();
        static readonly AsyncLocal<Tester> asyncLocalTester = new AsyncLocal<Tester>();

        static async Task ExamineValuesOfLocalObjectsEitherSideOfAwait(string expectedValue)
        {
            Assert.AreEqual(expectedValue, asyncLocalString.Value);
            await Task.Delay(100);
            Assert.AreEqual(expectedValue, asyncLocalString.Value);
        }
    }
}

注意 backgroundTask 如何能够看到与调用它的代码相同的异步本地(即使它来自另一个线程)。它也不会影响调用代码异步本地字符串或对象 - 因为它会重新分配给它们。但是调用代码可以看到它变成了Tester(证明Task和它的调用代码共享同一个Tester实例)。