【问题标题】:Does `AsyncLocal` also do the things that `ThreadLocal` does?`AsyncLocal` 也能做 `ThreadLocal` 做的事情吗?
【发布时间】:2020-07-24 12:19:54
【问题描述】:

我很难找到关于 AsyncLocal<T> 所做工作的简单文档。

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


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

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

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

即:

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

我知道对于MyAsyncMethod 的每个单独调用myAsyncLocal.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);
        }
    }

【问题讨论】:

  • (我以为这是因为Parallel.Foreach 正在重用一些线程,因此这些线程看到的是以前初始化(但不再使用)的StringHolder?)
  • 不管怎样,你明白即使没有But is it guaranteed that each of the invocations will be looking at different objects in the first place? 是假的吗?因为如果您在Parallel.ForEach 之前设置AsyncLocal,那么每次调用都会看到同一个对象(因此它们不是“不同的对象”)?还是我误会了?
  • 啊哈哈,是的,我明白你的意思了(并且同意提前设置 AsyncLocal 会导致所有孩子获得相同的对象,从而破坏彼此的断言)
  • 但是您的示例很有趣 - programmer.help/blogs/… 声明 After the thread is used, it will return to the thread pool. The status of asynclocal &lt; T &gt; will be cleared and the previous value cannot be accessed - 您的代码示例似乎反驳
  • @mjwills 我猜这是Parallel.ForEach() 的实现细节?也就是说,它实际上是在设置自己的个人“线程池”,而不是“放弃任何单独的线程”。因此,如果您“正常”使用线程,上面的引用是正确的?

标签: c# multithreading async-await thread-local-storage


【解决方案1】:

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实例)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-02-25
    • 2011-02-27
    • 1970-01-01
    • 2012-10-21
    • 2011-02-02
    • 1970-01-01
    • 2021-03-02
    • 2017-03-05
    相关资源
    最近更新 更多