.net core AsyncLocal mất giá trị
Tôi sử dụng một mẫu tương tự như HttpContextAccessor
Phiên bản đơn giản như sau, Console.WriteLine(SimpleStringHolder.StringValue)không được phép là null.
public class SimpleStringHolder
{
private static readonly AsyncLocal<ValueHolder> CurrentHolder = new AsyncLocal<ValueHolder>();
public static string StringValue
{
get => CurrentHolder.Value?.StringValue;
set
{
var holder = CurrentHolder.Value;
if (holder != null)
{
holder.StringValue = null;
}
if (value != null)
{
CurrentHolder.Value = new ValueHolder() { StringValue = value };
}
}
}
private class ValueHolder
{
public string StringValue;
}
}
class Program
{
private static readonly AsyncLocal<string> currentValue = new AsyncLocal<string>();
public static void Main(string[] args)
{
var task = Task.Run(async () => await OutterAsync());
task.Wait();
}
public static async Task OutterAsync()
{
SimpleStringHolder.StringValue = "1";
await InnerAsync();
Console.WriteLine(SimpleStringHolder.StringValue); //##### the value is gone ######
}
public static async Task InnerAsync()
{
var lastValue = SimpleStringHolder.StringValue;
await Task.Delay(1).ConfigureAwait(false);
SimpleStringHolder.StringValue = lastValue; // comment this line will make it work
Console.WriteLine(SimpleStringHolder.StringValue); //the value is still here
}
}
Trong đoạn mã trên, OutterAsyncgọi một phương pháp async InnerAsync, trong InnerAsyncnhững StringValueđược thiết lập, mà làm cho AsyncLocal mất ngữ cảnh của nó trong OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);là null.
Tôi nghĩ điều kỳ diệu nằm trong tập thuộc tính của SimpleStringHolder, việc xóa đoạn mã sau sẽ khiến mọi thứ trở nên ổn thỏa.
if (holder != null)
{
holder.StringValue = null;
}
Đoạn mã trên hoạt động như mong đợi.
Xin hãy giúp tôi hiểu đây là phép thuật gì?
Trả lời
AsyncLocal<T>tồn tại để cung cấp cơ chế bảo toàn các giá trị trong ngữ cảnh thực thi không đồng bộ. Chìa khóa cho điều này là hai yếu tố liên quan đến ví dụ của bạn:
- An
awaitcho phép một phương thức quay trở lại trình gọi, điều này có thể thay đổi ngữ cảnh. VớiThreadLocal<T>kiểu cũ hơn , khi việc thực thi trả về điều khiển cho phương thức, nó có thể nằm trong một luồng khác, mặc dù theoasyncquan điểm, ngữ cảnh là như nhau. Việc sử dụngAsyncLocal<T>đảm bảo rằng trạng thái của ngữ cảnh được khôi phục khiawaitđiều khiển trả về phương thức sau khi đối tượng chờ đã hoàn thành. - Cho đến khi một điều gì đó xảy ra yêu cầu bối cảnh thay đổi, trạng thái hiện tại của một
AsyncLocal<T>đối tượng là bất kể nó là gì trước đó. Tức là một phương thức về cơ bản kế thừa trạng thái của đối tượng khi nó được gọi. Nếu bạn đang xử lý các giá trị đơn giản, không có bất ngờ nào ẩn nấp, nhưng trong trường hợp có một kiểu tham chiếu nhưValueHolderkiểu của bạn , điều duy nhất cầnAsyncLocal<T>theo dõi là tham chiếu đến đối tượng đó. Vẫn chỉ có một bản sao của đối tượng và các thay đổi đối với trạng thái của bất kỳ đối tượng nào như vậy hoạt động như chúng luôn làm với hoặc không có ngữ cảnh không đồng bộ trôi nổi xung quanh (tức là chúng được nhìn thấy bởi bất kỳ tham chiếu nào đến đối tượng đó).
Vì vậy, trong ví dụ mã bạn đã cung cấp:
OutterAsync()đặt thuộcStringValuetính thành"1", dẫn đến mộtValueHolderđối tượng mới được tạo và thuộcStringValuetính của đối tượng đó được đặt thành"1".OutterAsync()cuộc gọiInnerAsync(). Phương thức đó sau đó lấystringtham chiếu từ chủ sở hữu (gián tiếp… tức là bằng cách đi qua thuộcSimpleStringHolder.StringValuetính). Vì không có thay đổi nào đối với giá trị cũng như ngữ cảnh đã được thực hiện tại thời điểm này, cùng mộtValueHolderđối tượng được sử dụng trong trường hợp này, vì vậy bạn sẽ"1"quay lại.InnerAsync()đang đợi một tác vụ không đồng bộ, khiến một ngữ cảnh thực thi mới được tạo ra nhằm mục đích tách biệt các thay đổi được thực hiệnAsyncValue<T>đối với đối tượng với ngữ cảnh đó. Kể từ thời điểm này, các thay đổi đối với đối tượng sẽ không được nhìn thấy bằng mã trong một ngữ cảnh khác. Ví dụ, mã thực thi trongOutterAsync()phương thức.- Sau khi hoàn thành tác vụ không đồng bộ
InnerAsync(), phương thức đó sẽ đặt một giá trị mới cho thuộcSimpleStringHolder.StringValuetính. Bởi vì ngữ cảnh trước đó đã được kế thừa, khi bộ thiết lập đặtholder.StringValuethànhnull, nó sẽ đặt thuộc tính của đối tượng được tạo trong đóOutterAsync(). Nhưng… bởi vì mã ở trong một ngữ cảnh mới, khi người thiết lập chỉ định một giá trị mới cho thuộcCurrentHolder.Valuetính, thay đổi đó sẽ bị tách biệt với ngữ cảnh đó. - Khi
InnerAsync()phương thức cuối cùng hoàn thành, điều này hoàn thành nhiệm vụ màOutterAsync()phương thứcawaitđang chờ đợi. Điều này gây ra việcAsyncValue<T>khôi phục trạng thái của nóOutterAsync()về ngữ cảnh của phương thức, khác với ngữ cảnh đã cóInnerAsync()khi nó cập nhậtSimpleStringHolder.StringValuegiá trị. Và cụ thể, trạng thái khôi phục này là một tham chiếu đếnValueHolderđối tượng ban đầu được đặt trong thờiSimpleStringHolderđiểm thuộcholder.StringValuetính được đặt thành null. - Vì vậy, khi
OutterAsync()xem xét giá trị thuộc tính, nó thấy nó được đặt thành null. Bởi vì nó đã được đặt thành null.
Trong thử nghiệm của riêng bạn, bạn có thể loại bỏ hoàn toàn phép gán null, hoặc đơn giản là bỏ qua phép gán SimpleStringHolder.StringValuesau câu lệnh InnerAsync()' await(vì nếu bạn không thực hiện phép gán, thì phép gán null sẽ không bao giờ được thực thi). Dù bằng cách nào, việc gán null không xảy ra và do đó giá trị được gán trước đó vẫn còn.
Nhưng nếu bạn thực hiện gán null, người gọi OutterAsync()sẽ khôi phục ngữ cảnh của nó và sau đó khôi phục tham chiếu đối tượng chủ và stringtham chiếu riêng của đối tượng chủ đó đã được đặt thành null, vì vậy đó là những gì có thể OutterAsync()thấy.
Bài đọc liên quan:
Tác dụng của AsyncLocal trong mã không đồng bộ / await là gì?
Tại sao AsyncLocal trả về các kết quả khác nhau khi mã được cấu trúc lại một chút?
Liệu AsyncLocalcòn làm những điều mà ThreadLocallàm?