Нужно ли вручную изменять значение переменной AsyncLocal на «Dispose» / ​​«Release» его по окончании логического запроса - PullRequest
4 голосов
/ 21 апреля 2020

Я прочитал о AsyncLocal<T> из документации MSDN , но один момент все еще неясен для меня.

Я работаю над чем-то вроде контекстного кеширования / запоминания , который имеет простую цель хранения данных через логический запрос. Это похоже на старый HttpContext.Current, где данные хранятся в запросе и будут опубликованы в конце запроса. В моем случае, однако, я хочу быть независимым от среды c, поэтому реализация не связана, например, с ASP. NET MVC, ASP. NET Core, WCF, et * 1030. *., хотя все еще имеет возможность хранить и извлекать данные, которые связаны с логическим запросом, не разделяя их между логически различными запросами.

Чтобы упростить мой код в соответствии с вопросом, он выглядит примерно так:

class ContextualStorageAccessor
{
    // ConcurrentDictionary since it's okay if some parallel operations are used per logical request 
    private readonly AsyncLocal<ConcurrentDictionary<string, object>> _csm = new AsyncLocal<ConcurrentDictionary<string, object>>();

    public ConcurrentDictionary<string, object> Storage
    { 
        get
        {
            if (_csm.Value != null)
                return _csm.Value;

            _csm.Value = new ConcurrentDictionary<string, object>();

            return _csm.Value;
        }
    } 
}

Жизненный цикл ContextualStorageAccessor является одноэлементным.

А теперь вопрос: у меня будет уникальный экземпляр Value на запрос? Другими словами, нужно ли продолжать присваивать значение по умолчанию для _csm.Value вручную? Или я могу полагаться на тип самого приложения (например, ASP. NET MVC, WCF и т. Д. c.), Который позаботится об этом?

Или, если перефразировать: где конец «асинхронного c потока» и гарантирует ли ExecutionContext уникальные значения для каждого логического вызова, которые будут автоматически аннулированы - в простом сценарии null будет быть назначенным на AsyncLocal.Value - к концу логического вызова (для ASP. NET MVC, веб-запрос; для WCF, операция), если используется AsyncLocal.Value?

1 Ответ

2 голосов
/ 21 апреля 2020

Если вы попробуете этот код, вы увидите, что каждый новый асинхронный поток c создает новое значение. Поэтому ответ должен быть: Да, у вас должно быть уникальное значение для каждого запроса.

private static readonly AsyncLocal<object> Item = new AsyncLocal<object>();

public static async Task Main()
{
    async Task Request()
    {
        if (Item.Value is {})
        {
            Console.WriteLine("This should never happen.");
            throw new InvalidOperationException("Value should be null here.");
        }

        Item.Value = new object();
    }

    await Task.Run(Request); // Just to be sure that Item.Value is initialized once.

    await Task.WhenAll(
        Task.Run(Request),
        Task.Run(Request),
        Task.Run(Request),
        Task.Run(Request),
        Task.Run(Request));

    Console.WriteLine("finished");
}

DEMO

Но я попробовал немного больше сложный пример, чтобы определить, где заканчивается асинхронный поток c. Код очень прост, но массовое использование Console.WriteLine делает его немного запутанным.

public class Program
{
    public static async Task Main()
    {
        await Task.Run(async () => 
            {
                Console.WriteLine("Async flow entered...");             

                // Init async value
                if (Cache.Instance.Item.Value is {})
                    throw new InvalidOperationException("The async flow has just startet. A value should not be initialized.");

                var newValue = new object();
                Console.WriteLine($"Create: value = #{RuntimeHelpers.GetHashCode(newValue)}");

                Cache.Instance.Item.Value = newValue;
                await Foo();

                Console.WriteLine("Async flow exitted.");
            });

        Console.WriteLine("Main finished.\n\n");
    }

    private static async Task Foo()
    {
        Console.WriteLine($"Foo: entered...");
        await Bar();

        Console.WriteLine($"Foo: getting value...");
        var knownValue = Cache.Instance.Item.Value;
        Console.WriteLine($"Foo: value = #{RuntimeHelpers.GetHashCode(knownValue)}");
        Console.WriteLine($"Foo: exitted.");
    }

    private static async Task Bar()
    {
        Console.WriteLine($"Bar: entered...");
        await Task.CompletedTask;
        Console.WriteLine($"Bar: exitted.");
    }
}

public sealed class Cache
{
    public static Cache Instance = new Cache();

    public AsyncLocal<object> Item { get; } = new AsyncLocal<object>(OnValueChanged);

    private static void OnValueChanged(AsyncLocalValueChangedArgs<object> args)
    {
        Console.WriteLine($"OnValueChanged! Prev: #{RuntimeHelpers.GetHashCode(args.PreviousValue)} Current: #{RuntimeHelpers.GetHashCode(args.CurrentValue)}");
    }
}

DEMO

Вывод этого кода:

Async flow entered...
Create: value = #6044116
OnValueChanged! Prev: #0 Current: #6044116
Foo: entered...
Bar: entered...
Bar: exitted.
Foo: getting value...
Foo: value = #6044116
Foo: exitted.
Async flow exitted.
OnValueChanged! Prev: #6044116 Current: #0
Main finished.

Ожидается поток значений. Это отвечает на вопрос , присваивается ли значение значению по умолчанию - да, это так. Поток asyn c заканчивается там, где заканчивается Task.Run, и в этот момент значение получает значение default.

Но все становится интереснее, если вы измените await Task.CompletedTask; в Bar на await Task.Delay(1); , Вывод выглядит очень по-разному:

Async flow entered...
Create: value = #6044116
OnValueChanged! Prev: #0 Current: #6044116
Foo: entered...
Bar: entered...
OnValueChanged! Prev: #6044116 Current: #0
OnValueChanged! Prev: #0 Current: #6044116
Bar: exitted.
Foo: getting value...
Foo: value = #6044116
Foo: exitted.
Async flow exitted.
OnValueChanged! Prev: #6044116 Current: #0
Main finished.


OnValueChanged! Prev: #0 Current: #6044116
OnValueChanged! Prev: #6044116 Current: #0

Странные детали начинаются после ввода Bar. Похоже, что await Task.Delay(1) нарушает асинхронный поток c. Но стоимость восстановлена ​​верно. Здесь я могу, по крайней мере, придумать объяснение, угадав.

И по-настоящему интересная вещь происходит после того, как main закончен. Значение восстанавливается и очищается еще раз ... Мне не хватает воображения здесь. Я совершенно не представляю, почему и как восстанавливается значение после завершения Task.Run, а также должен завершаться асинхронный поток c. Это дает мне ощущение, что G C не может очистить объект, пока программа не закончилась.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...