C# AsyncLocal
🏷️ C#
使用 ThreadLocal
可以很方便的隔离多线程中的数据,但是在调用了异步方法时,就无法正确获取父线程中设置的 ThreadLocal
中的值。
在官方文档 AsyncLocal<T>
Class 中的示例很清楚的说明了 ThreadLocal
和 AsyncLocal
的区别。
using System;
using System.Threading;
using System.Threading.Tasks;
class Example
{
static AsyncLocal<string> _asyncLocalString = new AsyncLocal<string>();
static ThreadLocal<string> _threadLocalString = new ThreadLocal<string>();
static async Task AsyncMethodA()
{
// Start multiple async method calls, with different AsyncLocal values.
// We also set ThreadLocal values, to demonstrate how the two mechanisms differ.
_asyncLocalString.Value = "Value 1";
_threadLocalString.Value = "Value 1";
var t1 = AsyncMethodB("Value 1");
_asyncLocalString.Value = "Value 2";
_threadLocalString.Value = "Value 2";
var t2 = AsyncMethodB("Value 2");
// Await both calls
await t1;
await t2;
}
static async Task AsyncMethodB(string expectedValue)
{
Console.WriteLine("Entering AsyncMethodB.");
Console.WriteLine(" Expected '{0}', AsyncLocal value is '{1}', ThreadLocal value is '{2}'",
expectedValue, _asyncLocalString.Value, _threadLocalString.Value);
await Task.Delay(100);
Console.WriteLine("Exiting AsyncMethodB.");
Console.WriteLine(" Expected '{0}', got '{1}', ThreadLocal value is '{2}'",
expectedValue, _asyncLocalString.Value, _threadLocalString.Value);
}
static async Task Main(string[] args)
{
await AsyncMethodA();
}
}
// The example displays the following output:
// Entering AsyncMethodB.
// Expected 'Value 1', AsyncLocal value is 'Value 1', ThreadLocal value is 'Value 1'
// Entering AsyncMethodB.
// Expected 'Value 2', AsyncLocal value is 'Value 2', ThreadLocal value is 'Value 2'
// Exiting AsyncMethodB.
// Expected 'Value 2', got 'Value 2', ThreadLocal value is ''
// Exiting AsyncMethodB.
// Expected 'Value 1', got 'Value 1', ThreadLocal value is ''
AsyncMethodB 方法中 await
后面的处理会在新的线程中执行,从而导致从 _threadLocalString 中无法获取到数据。
这是因为新的线程中还没有设置当前线程的该变量的值。
而 AsyncLocal
则是用来在异步处理处理中跨线程保存数据的。
根据上例的执行结果,await
后的处理中获取的 _asyncLocalString 的值是开启这个线程(示例中的 t1)时的值,之后再修改主线程中 _asyncLocalString 的值不会影响到这个线程(t1)中的值。
另外 AsyncLocal
还实现了一个可以指定值变更通知处理的构造函数,整个异步处理中的任一线程修改了值都会触发该处理。
2019/08/02 追记
使用 AsyncLocal
时会发生线程间数据的传递,那么在子线程或者父线程修改了数据是否会影响到另一个线程呢?
从上面官方的示例中其实已经展示了一点:在父线程中修改 AsyncLocal<string>
中的值时,子线程并没有随之更改。
为了确认是否会传递,将上面的代码修改如下:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncLocalSample
{
class Program
{
static AsyncLocal<string> _asyncLocalString = new AsyncLocal<string>();
static ThreadLocal<string> _threadLocalString = new ThreadLocal<string>();
static AsyncLocal<Dictionary<string, string>> _asyncLocalDictionary = new AsyncLocal<Dictionary<string, string>>();
static ThreadLocal<Dictionary<string, string>> _threadLocalDictionary = new ThreadLocal<Dictionary<string, string>>();
const string KEY = "KEY";
static async Task AsyncMethodA(string value)
{
Console.WriteLine("Exiting AsyncMethodA.");
Console.WriteLine($@" Expected '',
AsyncLocalString value is '{_asyncLocalString.Value}',
ThreadLocalString value is '{_threadLocalString.Value}',
AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
ThreadId is {Thread.CurrentThread.ManagedThreadId}");
// Start multiple async method calls, with different AsyncLocal values.
// We also set ThreadLocal values, to demonstrate how the two mechanisms differ.
_asyncLocalString.Value = value;
_threadLocalString.Value = value;
_asyncLocalDictionary.Value = new Dictionary<string, string>();
_asyncLocalDictionary.Value.Add(KEY, value);
_threadLocalDictionary.Value = new Dictionary<string, string>();
_threadLocalDictionary.Value.Add(KEY, value);
var t1 = AsyncMethodB(value);
// Await both calls
await t1;
await Task.Delay(100);
Console.WriteLine("Exiting AsyncMethodA.");
Console.WriteLine($@" Expected '{value}',
AsyncLocalString value is '{_asyncLocalString.Value}',
ThreadLocalString value is '{_threadLocalString.Value}',
AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
ThreadId is {Thread.CurrentThread.ManagedThreadId}");
}
static async Task AsyncMethodB(string expectedValue)
{
Console.WriteLine("Entering AsyncMethodB.");
Console.WriteLine($@" Expected '{expectedValue}',
AsyncLocalString value is '{_asyncLocalString.Value}',
ThreadLocalString value is '{_threadLocalString.Value}',
AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
ThreadId is { Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(100);
_asyncLocalString.Value = $"{expectedValue} new";
_threadLocalString.Value = $"{expectedValue} new";
_asyncLocalDictionary.Value[KEY] = $"{expectedValue} new";
if (_threadLocalDictionary.Value == null)
{
_threadLocalDictionary.Value = new Dictionary<string, string>();
_threadLocalDictionary.Value.Add(KEY, $"{expectedValue} new");
}
else
{
_threadLocalDictionary.Value[KEY] = $"{expectedValue} new";
}
Console.WriteLine("Entering AsyncMethodB.");
Console.WriteLine($@" Expected '{expectedValue} new',
AsyncLocalString value is '{_asyncLocalString.Value}',
ThreadLocalString value is '{_threadLocalString.Value}',
AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
ThreadId is { Thread.CurrentThread.ManagedThreadId}");
}
static void Main(string[] args)
{
var t1 = AsyncMethodA("Value 1");
t1.Wait();
Thread.Sleep(1000);
Console.WriteLine("----------------------------------------");
var t2 = AsyncMethodA("Value 2");
t2.Wait();
Console.ReadLine();
}
}
}
运行结果:
AsyncLocal<Dictionary<string, string>>
中的值在子线程中修改,同时也改变了父线程中的值。但是 AsyncLocal<string>
就没有这个效果,子线程中的修改并没有传递到父线程。
这说明在创建子线程时,AsyncLocal
会复制父线程中 AsyncLocal.Value
的地址到子线程的 AsyncLocal.Value
,也就是仅拷贝了引用的地址。
如果在子线程中修改了 AsyncLocal.Value
中的值则会同步的影响到父线程的值,因为指向的是堆中的同一个数据;如果重新指定了 AsyncLocal.Value
所指向的地址(比如设置为 null
或者 一个新的实例),则不会影响父线程的值,因为仅修改了当前线程中指向的地址,而并没有改变修改前地址指向的数据。