.net core AsyncLocal mất giá trị

Nov 13 2020

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

3 PeterDuniho Nov 13 2020 at 12:43

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:

  1. 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ới ThreadLocal<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ù theo asyncquan điểm, ngữ cảnh là như nhau. Việc sử dụng AsyncLocal<T>đảm bảo rằng trạng thái của ngữ cảnh được khôi phục khi awaitđiều khiển trả về phương thức sau khi đối tượng chờ đã hoàn thành.
  2. 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ần AsyncLocal<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:

  1. OutterAsync()đặt thuộc StringValuetính thành "1", dẫn đến một ValueHolderđối tượng mới được tạo và thuộc StringValuetính của đối tượng đó được đặt thành "1".
  2. OutterAsync()cuộc gọi InnerAsync(). Phương thức đó sau đó lấy stringtham chiếu từ chủ sở hữu (gián tiếp… tức là bằng cách đi qua thuộc SimpleStringHolder.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ột ValueHolderđối tượng được sử dụng trong trường hợp này, vì vậy bạn sẽ "1"quay lại.
  3. 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ện AsyncValue<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 trong OutterAsync()phương thức.
  4. 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ộc SimpleStringHolder.StringValuetính. Bởi vì ngữ cảnh trước đó đã được kế thừa, khi bộ thiết lập đặt holder.StringValuethành null, 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ộc CurrentHolder.Valuetính, thay đổi đó sẽ bị tách biệt với ngữ cảnh đó.
  5. 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ức awaitđang chờ đợi. Điều này gây ra việc AsyncValue<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ật SimpleStringHolder.StringValuegiá trị. Và cụ thể, trạng thái khôi phục này là một tham chiếu đến ValueHolderđối tượng ban đầu được đặt trong thời SimpleStringHolderđiểm thuộc holder.StringValuetính được đặt thành null.
  6. 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?