`AsyncLocal` 是否也做 `ThreadLocal` 做的事情?
Does `AsyncLocal` also do the things that `ThreadLocal` does?
我正在努力寻找有关 AsyncLocal<T>
功能的简单文档。
我写了一些测试,我 认为 告诉我答案是“是”,但如果有人能证实这一点,那就太好了! (特别是因为我不知道如何编写可以明确控制线程和延续上下文的测试......所以它们可能只是巧合!)
据我了解,ThreadLocal
将保证如果您在不同的线程上,那么您将获得对象的不同实例。
- 如果您正在创建和结束线程,那么您可能会在稍后再次重新使用该线程(并因此到达一个线程,其中“该线程的”
ThreadLocal
对象已经被使用了一点).
- 但是与
await
的互动就不那么愉快了。您继续的线程(即使 .ConfigureAwait(true)
)不能保证与您开始的线程相同,因此您可能无法从另一侧的 ThreadLocal
中获取相同的对象。
相反,AsyncLocal
确实保证您将在await
调用的任何一侧获得相同的对象。
但是我找不到任何地方说 AsyncLocal
首先会得到一个特定于初始线程的值!
即:
- 假设您有一个实例方法 (
MyAsyncMethod
) 从其 class 的任何一侧引用 'shared' AsyncLocal
字段 (myAsyncLocal
) await
个电话。
- 并假设您采用 class 的一个实例并多次并行调用该方法。 * 最后假设每次调用都恰好安排在不同的线程上。
我知道对于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?
没有。从逻辑上将其视为传递给函数的参数(不是 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
我不确定 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
实例)。
我正在努力寻找有关 AsyncLocal<T>
功能的简单文档。
我写了一些测试,我 认为 告诉我答案是“是”,但如果有人能证实这一点,那就太好了! (特别是因为我不知道如何编写可以明确控制线程和延续上下文的测试......所以它们可能只是巧合!)
据我了解,
ThreadLocal
将保证如果您在不同的线程上,那么您将获得对象的不同实例。- 如果您正在创建和结束线程,那么您可能会在稍后再次重新使用该线程(并因此到达一个线程,其中“该线程的”
ThreadLocal
对象已经被使用了一点). - 但是与
await
的互动就不那么愉快了。您继续的线程(即使.ConfigureAwait(true)
)不能保证与您开始的线程相同,因此您可能无法从另一侧的ThreadLocal
中获取相同的对象。
- 如果您正在创建和结束线程,那么您可能会在稍后再次重新使用该线程(并因此到达一个线程,其中“该线程的”
相反,
AsyncLocal
确实保证您将在await
调用的任何一侧获得相同的对象。
但是我找不到任何地方说 AsyncLocal
首先会得到一个特定于初始线程的值!
即:
- 假设您有一个实例方法 (
MyAsyncMethod
) 从其 class 的任何一侧引用 'shared'AsyncLocal
字段 (myAsyncLocal
)await
个电话。 - 并假设您采用 class 的一个实例并多次并行调用该方法。 * 最后假设每次调用都恰好安排在不同的线程上。
我知道对于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?
没有。从逻辑上将其视为传递给函数的参数(不是 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
我不确定 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
实例)。