Установка 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)