Подтвердить что ты не робот

Безопасность AsyncLocal в ядре ASP.NET

Для .NET Core AsyncLocal является заменой CallContext. Однако неясно, как "безопасно" использовать его в ядре ASP.NET.

В ASP.NET 4 (MVC 5) и более ранней версии модель гибкости потоков ASP.NET сделала CallContext неустойчивой. Таким образом, в ASP.NET единственным безопасным способом достижения поведения логического контекста для каждого запроса было использование HttpContext.Current.Items. Под обложками HttpContext.Current.Items реализуется с помощью CallContext, но это делается безопасным способом для ASP.NET.

Напротив, в контексте Web-API OWIN/Katana модель гибкости потоков не была проблемой. Я смог безопасно использовать CallContext после тщательного рассмотрения того, как правильно его распоряжаться.

Но теперь я имею дело с ASP.NET Core. Я хотел бы использовать следующее промежуточное ПО:

public class MultiTenancyMiddleware
{
    private readonly RequestDelegate next;
    static int random;

    private static AsyncLocal<string> tenant = new AsyncLocal<string>();
    //This is the new form of "CallContext".
    public static AsyncLocal<string> Tenant
    {
        get { return tenant; }
        private set { tenant = value; }
    }

    //This is the new verion of [ThreadStatic].
    public static ThreadLocal<string> LocalTenant;

    public MultiTenancyMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        //Just some garbage test value...
        Tenant.Value = context.Request.Path + random++;
        await next.Invoke(context);

        //using (LocalTenant = new AsyncLocal<string>()) { 
        //    Tenant.Value = context.Request.Path + random++;
        //    await next.Invoke(context);
        //}
    }
}

До сих пор приведенный выше код, похоже, работает нормально. Но есть хотя бы один красный флаг. Раньше было важно обеспечить, чтобы CallContext рассматривался как ресурс, который должен быть освобожден после каждого вызова.

Теперь я вижу, что нет очевидного способа "очистить" AsyncLocal.

Я включил код, закомментировал, показывая, как работает ThreadLocal<T>. Это IDisposable, и поэтому он имеет очевидный механизм очистки. Напротив, AsyncLocal не IDisposable. Это раздражает.

Это потому, что AsyncLocal еще не находится в состоянии кандидат-релиз? Или это потому, что действительно не нужно выполнять очистку?

И даже если AsyncLocal правильно используется в моем примере выше, существуют ли какие-либо проблемы с маневренностью в старой школе в ASP.NET Core, которые сделают это промежуточное программное обеспечение неработоспособным?

Специальное примечание

Для тех, кто не знаком с проблемами CallContext, в приложениях ASP.NET, этот пост SO, Джон Скит ссылается на углубленное обсуждение проблемы (в свою очередь ссылается на комментарий от Scott Hanselman). Эта "проблема" не является ошибкой - это просто обстоятельство, которое необходимо тщательно учитывать.

Кроме того, я лично могу подтвердить это несчастливое поведение. Когда я создаю приложения ASP.NET, я обычно включаю тесты нагрузки как часть моей инфраструктуры тестирования автоматизации. Во время нагрузочных тестов я могу наблюдать, что CallContext становится нестабильным (где, возможно, от 2% до 4% запросов показывают повреждение CallContext. Я также видел случаи, когда веб-API GET имеет стабильное поведение CallContext но операции POST все нестабильны. Единственный способ добиться полной стабильности - полагаться на HttpContext.Current.Items.

Однако в случае ASP.NET Core я не могу полагаться на HttpContext.Items... нет такой точки статического доступа. Я также пока не могу создавать тесты нагрузки для приложений .NET Core, с которыми я занимаюсь, отчасти поэтому я не ответил на этот вопрос сам.:)

Снова: Пожалуйста, поймите, что "нестабильность" и "проблема", которые я обсуждаю, не являются ошибкой вообще. CallContext не является чем-то испорченным. Проблема просто является следствием модели отправки потоков, используемой ASP.NET. Решение состоит в том, чтобы знать, что проблема существует, и соответствующим образом кодировать (например, использовать HttpContext.Current.Items вместо CallContext, когда внутри приложения ASP.NET).

Моя цель с этим вопросом заключается в том, чтобы понять, как эта динамика применяется (или нет) в ASP.NET Core, так что я случайно не создаю нестабильный код при использовании новой конструкции AsyncLocal.

4b9b3361

Ответ 1

Я просто изучаю исходный код класса ExecutionContext для CoreClr: https://github.com/dotnet/coreclr/blob/775003a4c72f0acc37eab84628fcef541533ba4e/src/mscorlib/src/System/Threading/ExecutionContext.cs

Основываясь на моем понимании кода, локальные значения async являются полями/переменными каждого экземпляра ExecutionContext. Они не основаны на потоковом хранилище данных ThreadLocal или любого конкретного потока.

Чтобы убедиться в этом, в моем тестировании с потоками пулов потоков экземпляр, оставшийся в локальном значении асинхронного доступа, недоступен, когда один и тот же поток потока потоков повторно используется, а "левый" деструктор экземпляра для очистки сам вызван в следующий GC цикл, то есть экземпляр GCed, как ожидалось.