Почему "Ожидание Task.Yield()" требуется для корректного потока Thread.CurrentPrincipal?

Код ниже был добавлен в недавно созданный проект Visual Studio 2012.NET 4.5 WebAPI.

Я пытаюсь назначить как HttpContext.Current.User, так и Thread.CurrentPrincipal в асинхронном методе. Назначение Thread.CurrentPrincipal выполняется некорректно, если не выполняется await Task.Yield(); (или что-то еще асинхронное) (передача true в AuthenticateAsync() приведет к успеху).

Почему это?

using System.Security.Principal;
using System.Threading.Tasks;
using System.Web.Http;

namespace ExampleWebApi.Controllers
{
    public class ValuesController : ApiController
    {
        public async Task GetAsync()
        {
            await AuthenticateAsync(false);

            if (!(User is MyPrincipal))
            {
                throw new System.Exception("User is incorrect type.");
            }
        }

        private static async Task AuthenticateAsync(bool yield)
        {
            if (yield)
            {
                // Why is this required?
                await Task.Yield();
            }

            var principal = new MyPrincipal();
            System.Web.HttpContext.Current.User = principal;
            System.Threading.Thread.CurrentPrincipal = principal;
        }

        class MyPrincipal : GenericPrincipal
        {
            public MyPrincipal()
                : base(new GenericIdentity("<name>"), new string[] {})
            {
            }
        }
    }
}

Примечания:

  • await Task.Yield(); может отображаться в любом месте AuthenticateAsync() или его можно переместить в GetAsync() после вызова AuthenticateAsync(), и он все равно будет успешным.
  • ApiController.User возвращает Thread.CurrentPrincipal.
  • HttpContext.Current.User всегда течет правильно, даже без await Task.Yield().
  • Web.config включает <httpRuntime targetFramework="4.5"/> который подразумевает UseTaskFriendlySynchronizationContext.
  • Я попросил аналогичный вопрос пару дней назад, но не понял, что этот пример только преуспел, потому что присутствовал Task.Delay(1000).

Ответ 1

Как интересно! Похоже, что Thread.CurrentPrincipal основан на контексте логического вызова, а не на контексте вызовов для потоков. ИМО это совершенно неинтуитивно, и мне было бы интересно узнать, почему это было реализовано таким образом.


В .NET 4.5., async методы взаимодействуют с контекстом логического вызова, так что он будет более корректно работать с методами async. У меня есть запись в блоге; AFAIK, это единственное место, где оно задокументировано. В .NET 4.5, в начале каждого метода async, он активирует поведение "copy-on-write" для контекста логического вызова. Когда (если) изменяется контекст логического вызова, он сначала создает локальную копию.

Вы можете увидеть "локальность" контекста логического вызова (то есть, была ли она скопирована), наблюдая System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope в окне просмотра.

Если вы не Yield, то при установке Thread.CurrentPrincipal вы создаете копию контекста логического вызова, который рассматривается как "локальный" для этого метода async. Когда метод async возвращается, этот локальный контекст отбрасывается и исходный контекст занимает свое место (вы можете видеть ExecutionContextBelongsToCurrentScope, возвращающийся на false).

С другой стороны, если вы выполняете Yield, то поведение SynchronizationContext берет верх. На самом деле происходит то, что HttpContext фиксируется и используется для возобновления обоих методов. В этом случае вы не видите Thread.CurrentPrincipal, сохраненный от AuthenticateAsync до GetAsync; то, что на самом деле происходит, HttpContext сохраняется, а затем HttpContext.User перезаписывает Thread.CurrentPrincipal до возобновления методов.

Если вы переместите Yield в GetAsync, вы увидите аналогичное поведение: Thread.CurrentPrincipal рассматривается как локальная модификация с областью AuthenticateAsync; он возвращает значение, когда возвращается этот метод. Тем не менее, HttpContext.User по-прежнему устанавливается правильно, и это значение будет записано Yield, и когда метод возобновится, он перезапишет Thread.CurrentPrincipal.