Установка Thread.CurrentPrincipal с помощью async/await

Ниже приведена упрощенная версия, где я пытаюсь установить Thread.CurrentPrincipal в асинхронном методе для пользовательского объекта UserPrincipal, но пользовательский объект теряется после выхода из await, даже если он все еще находится в новом идентификаторе потока 10.

Есть ли способ изменить Thread.CurrentPrincipal в пределах ожидания и использовать его позже, не передавая и не возвращая его? Или это небезопасно и никогда не должно быть асинхронным? Я знаю, что есть изменения потока, но думал, что async/await будет обрабатывать синхронизацию для меня.

[TestMethod]
public async Task AsyncTest()
{
    var principalType = Thread.CurrentPrincipal.GetType().Name;
    // principalType = WindowsPrincipal
    // Thread.CurrentThread.ManagedThreadId = 11

    await Task.Run(() =>
    {
        // Tried putting await Task.Yield() here but didn't help

        Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity);
        principalType = Thread.CurrentPrincipal.GetType().Name;
        // principalType = UserPrincipal
        // Thread.CurrentThread.ManagedThreadId = 10
    });
    principalType = Thread.CurrentPrincipal.GetType().Name;
    // principalType = WindowsPrincipal (WHY??)
    // Thread.CurrentThread.ManagedThreadId = 10
}

5 ответов

Решение

Вы можете использовать собственный ожидающий поток CurrentPrincipal (или любые свойства потока, в этом отношении). Приведенный ниже пример показывает, как это можно сделать, вдохновленный Стивеном Таубом.CultureAwaiter, Оно использует TaskAwaiter внутренне, поэтому контекст синхронизации (если есть) также будет захвачен.

Использование:

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);

await TaskExt.RunAndFlowPrincipal(() => 
{
    Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity);
    Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);
    return 42;
});

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);

Код (только очень слегка проверенный):

public static class TaskExt
{
    // flowing Thread.CurrentPrincipal
    public static FlowingAwaitable<TResult, IPrincipal> RunAndFlowPrincipal<TResult>(
        Func<TResult> func,
        CancellationToken token = default(CancellationToken))
    {
        return RunAndFlow(
            func,
            () => Thread.CurrentPrincipal, 
            s => Thread.CurrentPrincipal = s,
            token);
    }

    // flowing anything
    public static FlowingAwaitable<TResult, TState> RunAndFlow<TResult, TState>(
        Func<TResult> func,
        Func<TState> saveState, 
        Action<TState> restoreState,
        CancellationToken token = default(CancellationToken))
    {
        // wrap func with func2 to capture and propagate exceptions
        Func<Tuple<Func<TResult>, TState>> func2 = () =>
        {
            Func<TResult> getResult;
            try
            {
                var result = func();
                getResult = () => result;
            }
            catch (Exception ex)
            {
                // capture the exception
                var edi = ExceptionDispatchInfo.Capture(ex);
                getResult = () => 
                {
                    // re-throw the captured exception 
                    edi.Throw(); 
                    // should never be reaching this point, 
                    // but without it the compiler whats us to 
                    // return a dummy TResult value here
                    throw new AggregateException(edi.SourceException);
                }; 
            }
            return new Tuple<Func<TResult>, TState>(getResult, saveState());    
        };

        return new FlowingAwaitable<TResult, TState>(
            Task.Run(func2, token), 
            restoreState);
    }

    public class FlowingAwaitable<TResult, TState> :
        ICriticalNotifyCompletion
    {
        readonly TaskAwaiter<Tuple<Func<TResult>, TState>> _awaiter;
        readonly Action<TState> _restoreState;

        public FlowingAwaitable(
            Task<Tuple<Func<TResult>, TState>> task, 
            Action<TState> restoreState)
        {
            _awaiter = task.GetAwaiter();
            _restoreState = restoreState;
        }

        public FlowingAwaitable<TResult, TState> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _awaiter.IsCompleted; }
        }

        public TResult GetResult()
        {
            var result = _awaiter.GetResult();
            _restoreState(result.Item2);
            return result.Item1();
        }

        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(continuation);
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(continuation);
        }
    }
}

Я знаю, что есть изменения потока, но думал, что async/await будет обрабатывать синхронизацию для меня.

async / await сама по себе не выполняет синхронизацию локальных данных потока. Однако у него есть своего рода "крючок", если вы хотите выполнить собственную синхронизацию.

По умолчанию, когда вы await задача, она захватит текущий "контекст" (который SynchronizationContext.Current если это не null в каком случае это TaskScheduler.Current). Когда async метод возобновляется, он будет возобновлен в этом контексте.

Итак, если вы хотите определить "контекст", вы можете сделать это, определив свой собственный SynchronizationContext, Это не совсем легко, хотя. Особенно, если ваше приложение должно работать в ASP.NET, для которого требуется собственный AspNetSynchronizationContext (и они не могут быть вложенными или что-то еще - вы получаете только один). ASP.NET использует его SynchronizationContext установить Thread.CurrentPrincipal,

Тем не менее, обратите внимание, что есть определенное движение от SynchronizationContext, ASP.NET vNext не имеет такового. Овин никогда не делал (AFAIK). Самостоятельный SignalR тоже нет. Обычно считается более уместным передать значение каким-либо образом - будь то явное для метода или введенное в переменную-член типа, содержащего этот метод.

Если вы действительно не хотите передавать значение, то вы можете использовать и другой подход: async -эквивалент ThreadLocal, Основная идея заключается в хранении неизменных значений в LogicalCallContext, который соответствующим образом наследуется асинхронными методами. Я покрываю это "AsyncLocal" в моем блоге (есть слухи о AsyncLocal возможно, в.NET 4.6, но до тех пор вы должны накатить свой). Обратите внимание, что вы не можете читать Thread.CurrentPrincipal с использованием AsyncLocal техника; вам придется изменить весь ваш код, чтобы использовать что-то вроде MyAsyncValues.CurrentPrincipal,

Thread.CurrentPrincipal хранится в ExecutionContext, который хранится в локальном хранилище потока.

При выполнении делегата в другом потоке (с помощью Task.Run или ThreadPool.QueueWorkItem) ExecutionContext захватывается из текущего потока, а делегат переносится в ExecutionContext.Run. Поэтому, если вы установите CurrentPrincipal перед вызовом Task.Run, он все равно будет установлен внутри делегата.

Теперь ваша проблема заключается в том, что вы изменяете CurrentPrincipal внутри Task.Run, и ExecutionContext передается только в одну сторону. Я думаю, что это ожидаемое поведение в большинстве случаев, решение было бы установить CurrentPrincipal в начале.

То, что вы изначально хотели, невозможно при изменении ExecutionContext внутри Задачи, поскольку Task.ContinueWith также захватывает ExecutionContext. Чтобы сделать это, вы должны были бы каким-то образом захватить ExecutionContext сразу после запуска Делегата, а затем передать его обратно в продолжение пользовательского ожидающего, но это было бы очень плохо.

ExecutionContext, который содержит SecurityContext, который содержит CurrentPrincipal, почти всегда протекает через все асинхронные вилки. Так в вашем Task.Run() делегат, вы - в отдельной ветке, как вы заметили, получите то же самое CurrentPrincipal, Однако изнутри вы получаете контекст, передаваемый через ExecutionContext.Run (...), в котором говорится:

Контекст выполнения возвращается в свое предыдущее состояние, когда метод завершается.

Я нахожусь в странной территории, отличающейся от Стивена Клири:), но я не понимаю, как SynchronizationContext имеет какое-либо отношение к этому.

Стивен Тауб освещает большую часть этого в отличной статье здесь.

Я работаю над широко используемым программным продуктом asp.net, который существует уже более 10 лет... задолго до того, как был представлен async/await - теперь это .net Framework 4.8. В некоторых его частях использовалась настройка, подобная тем, которые используются в опросах, где мы используем поток для настройки принципала:

      public static void SetContext(int id, string name)
{
    var context = new LoadContext(id);
    var culture = context.Region.CultureInfo;

    Thread.CurrentPrincipal = new PTPrincipal(name, context);
    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
}

У этого есть проблема, аналогичная проблемам с задающими вопросы: при использовании асинхронного ожидания эта настройка контекста теряется. Исправлено то, что ожидаемые вызовы используют ConfigurationAwait(false) для каждого отдельного ожидаемого вызова прямо через стек вызовов, т.е.

      private async Task DoSomething()
{
    await Task.Delay(1000).ConfigureAwait(false);
}

.....

await DoSomething().ConfigureAwait(false);

Итак, для нас решение состоит в том, чтобы всегда использовать.ConfigureAwait(false)

Другие вопросы по тегам