.net 코어 AsyncLocal의 가치 상실

Nov 13 2020

HttpContextAccessor 와 유사한 패턴을 사용합니다.

단순화 된 버전은 다음과 같으며 Console.WriteLine(SimpleStringHolder.StringValue)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
    }
}

위의 코드에서, OutterAsync비동기 메소드를 호출 InnerAsync에서 InnerAsyncStringValueAsyncLocal는 그것의 문맥을 상실하게되는 설정 OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);널.

마법은 SimpleStringHolder의 속성 집합에 있다고 생각하며 다음 코드를 제거하면 문제가 해결됩니다.

if (holder != null)
{
    holder.StringValue = null;
}

위의 코드는 예상대로 작동합니다.

이게 무슨 마법인지 이해하도록 도와주세요.

답변

3 PeterDuniho Nov 13 2020 at 12:43

AsyncLocal<T>비동기 실행 컨텍스트 내에서 값을 보존하는 메커니즘을 제공하기 위해 존재합니다. 이에 대한 핵심은 귀하의 예와 관련된 두 가지 요소입니다.

  1. await사용하면 메서드가 호출자에게 반환되어 컨텍스트를 변경할 수 있습니다. 이전 ThreadLocal<T>유형을 사용하면 실행이 메서드에 제어를 반환 할 때 async컨텍스트가 동일 하더라도 다른 스레드에있을 수 있습니다 . 를 사용 AsyncLocal<T>하면 await대기 가능한 개체가 완료된 후 메서드에 컨트롤 이 반환 될 때 컨텍스트의 상태가 복원됩니다 .
  2. 컨텍스트를 변경해야하는 상황이 발생할 때까지 AsyncLocal<T>개체 의 현재 상태 는 이전 상태가됩니다. 즉, 메서드는 기본적으로 개체가 호출되었을 때의 상태를 상속합니다. 간단한 값을 처리하는 경우 놀라움이 숨어 있지 않지만 유형과 같은 참조 유형의 경우 추적 ValueHolder하는 유일한 것은 해당 객체에 AsyncLocal<T>대한 참조 입니다. 객체의 복사본은 여전히 ​​하나 뿐이며, 주어진 객체의 상태 변경은 항상 비동기 컨텍스트가 떠 다니는 것과 함께 또는없는 것처럼 작동합니다 (즉, 해당 객체에 대한 참조로 표시됨).

따라서 코드 예제에서 다음을 제공했습니다.

  1. OutterAsync()StringValue속성을로 설정하면 "1"ValueHolder객체가 생성 StringValue되고 해당 객체 의 속성이로 설정됩니다 "1".
  2. OutterAsync()전화 InnerAsync(). 그런 다음 해당 메서드 string는 소유자로부터 참조 를 검색합니다 (간접적으로 ... 즉 SimpleStringHolder.StringValue속성을 통해 이동 ). 이 시점에서 값이나 컨텍스트가 변경되지 않았으므로이 경우 동일한 ValueHolder객체가 사용되므로 "1"다시 돌아옵니다.
  3. InnerAsync()비동기 작업을 기다립니다 AsyncValue<T>.이 경우 해당 컨텍스트 에 대한 개체 변경 사항을 격리하기 위해 새 실행 컨텍스트가 생성 됩니다. 이 시점부터 객체의 변경 사항 은 다른 컨텍스트의 코드에서 볼 수 없습니다 . 예를 들어 OutterAsync()메서드 에서 실행되는 코드 입니다.
  4. 에서 비동기 작업이 완료된 후 InnerAsync()해당 메서드는 SimpleStringHolder.StringValue속성에 새 값을 설정 합니다. 이전 콘텍스트가 상속 때문에 세터 세트 때 holder.StringValue하는 null그것이에서 생성 된 객체의 속성을 설정한다 OutterAsync(). 그러나 ... 코드가 새 컨텍스트에 있기 때문에 setter가 새 값을 CurrentHolder.Value속성에 할당하면 해당 변경 내용이 해당 컨텍스트에 격리됩니다.
  5. InnerAsync()방법이 마침내 완료의이 완료 작업하는 것이 OutterAsync()방법의은 await에 기다리고 있었다. 이로 인해 는 값을 업데이트 할 때 있었던 컨텍스트와 다른 메서드의 컨텍스트로 AsyncValue<T>상태를 복원 합니다. 특히이 복원 된 상태는 속성이 null로 설정 되었을 때 원래 설정된 객체에 대한 참조 입니다.OutterAsync()InnerAsync()SimpleStringHolder.StringValueValueHolderSimpleStringHolderholder.StringValue
  6. 따라서 OutterAsync()속성 값을 살펴보면 null로 설정된 것을 찾습니다. null로 설정 되었기 때문입니다.

자신의 실험에서 null 할당을 완전히 제거하거나 의 문 SimpleStringHolder.StringValue뒤에 할당을 생략 할 수 있습니다 (할당을 하지 않으면 null 할당이 실행되지 않기 때문입니다). 어느 쪽이든 null 할당이 발생하지 않으므로 이전에 할당 된 값이 유지됩니다.InnerAsync()await

그러나 null 할당을 수행하면 호출자 OutterAsync()가 컨텍스트를 복원하고 홀더 개체 참조를 복원 한 다음 해당 홀더 개체의 자체 string참조가 이미로 설정되어 null있으므로 이것이 표시 OutterAsync()됩니다.

관련 읽기 :
비 비동기 / 대기 코드에서 AsyncLocal의 효과는 무엇입니까?
코드가 약간 리팩터링 될 때 AsyncLocal이 다른 결과를 반환하는 이유는 무엇입니까?
않습니다 AsyncLocal도 일하지 ThreadLocal않습니다를?