`AsyncLocal` 也能做 `ThreadLocal` 所做的事情吗?

Rei*_*l-- 12 c# multithreading thread-local-storage async-await

我正在努力寻找简单的文档来说明其AsyncLocal<T>作用。

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


  • 据我了解,ThreadLocal将保证如果您在不同的线程上,那么您将获得对象的不同实例。

    • 如果您正在创建并结束线程,那么您最终可能会在稍后再次重新使用该线程(从而到达“该线程的”ThreadLocal对象已经被使用过的线程)。
    • 但与人的互动await却不太愉快。您继续的线程(即使)不能保证与您开始的线程相同,因此您可能无法从另一端.ConfigureAwait(true)返回相同的对象。ThreadLocal
  • 相反,AsyncLocal 确实保证您将在调用的两侧获得相同的对象await

但我找不到任何地方实际上说AsyncLocal将获得特定于初始线程的值,首先!

IE:

  • 假设您有一个实例方法 ( MyAsyncMethod),它在调用的任一侧引用其类中的“共享”AsyncLocal字段 ( ) 。myAsyncLocalawait
  • 假设您获取该类的一个实例并并行调用该方法多次。* 最后假设每个调用最终都安排在不同的线程上。

我知道,对于 ,的每次单独调用MyAsyncMethodmyAsyncLocal.Value将在等待之前和之后返回相同的对象(假设没有任何内容重新分配它)

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


正如一开始提到的,我创建了一个测试来尝试自己确定这一点。以下测试一致通过

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

Run Code Online (Sandbox Code Playgroud)

mjw*_*lls 9

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

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

所以在你的代码示例中有:

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
Run Code Online (Sandbox Code Playgroud)

我不确定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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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