Почему SynchronizationContext по умолчанию не фиксируется в консольном приложении?

Я пытаюсь узнать больше о SynchronizationContext, поэтому я сделал это простое консольное приложение:

private static void Main()
{
    var sc = new SynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(sc);
    DoSomething().Wait();
}

private static async Task DoSomething()
{
    Console.WriteLine(SynchronizationContext.Current != null); // true
    await Task.Delay(3000);
    Console.WriteLine(SynchronizationContext.Current != null); // false! why ?
}

Если я правильно понимаю, оператор await захватывает текущий SynchronizationContext, а затем отправляет ему остальную часть асинхронного метода.

Однако в моем приложении SynchronizationContext.Current имеет значение null после await. Это почему ?

РЕДАКТИРОВАТЬ:

Даже когда я использую свой SynchronizationContext, он не захватывается, хотя его функция Post вызывается. Вот мой СЦ:

public class MySC : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        base.Post(d, state);
        Console.WriteLine("Posted");
    }
}

И вот как я его использую:

var sc = new MySC();
SynchronizationContext.SetSynchronizationContext(sc);

Спасибо!


person KeyBored    schedule 07.10.2018    source источник
comment
Ссылка: сначала попробуйте получить текущий контекст синхронизации . Если текущий контекст действительно является просто базовым типом SynchronizationContext, который должен быть эквивалентен отсутствию текущего контекста SynchronizationContext вообще, игнорируйте его. Это помогает повысить производительность, избегая ненужных публикаций и очередей рабочих элементов, но более того, это гарантирует, что если код публикует контекст по умолчанию как текущий, это не предотвратит использование текущего планировщика задач, если он есть.   -  person user4003407    schedule 07.10.2018
comment
P.S. Ваша Post реализация неверна. Он должен гарантировать, что делегат вызывается в правильном контексте. base.Post не делает этого за вас.   -  person user4003407    schedule 07.10.2018
comment
@PetSerAl Если текущий контекст действительно является просто базовым типом контекста синхронизации ..., но MySC не является базовым типом SC; означает, что syncCtx.GetType() != typeof(SynchronizationContext) верно.   -  person KeyBored    schedule 07.10.2018
comment
@PetSerAl И да, мой Post неверен .. это просто фальшивая реализация в учебных целях :)   -  person KeyBored    schedule 07.10.2018
comment
Как вы говорите, ваш Post называется. На этом захват контекста заканчивается. Он отвечает только за вызов Post в захваченном контексте и ничего больше. Все остальное Post ответственность исполнителя.   -  person user4003407    schedule 07.10.2018


Ответы (3)


Слово «захват» слишком непрозрачно, оно звучит слишком похоже на то, что должно быть реализовано во фреймворке. Вводит в заблуждение, поскольку обычно это происходит в программе, которая использует одну из реализаций SynchronizationContext по умолчанию. Как и один вы попадаете в приложение Winforms. Но когда вы пишете свой собственный, фреймворк больше не помогает, и это становится вашей работой.

Связь async / await дает контексту возможность запустить продолжение (код после ожидания) в определенном потоке. Звучит банально, раз уж вы так часто делали раньше, но на самом деле это довольно сложно. Невозможно произвольно прервать код, выполняемый этим потоком, что могло бы вызвать ужасные ошибки повторного входа. Поток должен помочь, он должен решить стандартную проблему производителя-потребителя. Принимает потокобезопасную очередь и цикл, который очищает эту очередь, обрабатывая запросы вызова. Задача переопределенных методов Post и Send - добавлять запросы в очередь, задача потока - использовать цикл для его очистки и выполнения запросов.

Основной поток приложения Winforms, WPF или UWP имеет такой цикл, он выполняется Application.Run (). С соответствующим контекстом синхронизации, который знает, как подавать в него запросы на вызов, соответственно WindowsFormsSynchronizationContext, DispatcherSynchronizationContext и WinRTSynchronizationContext. ASP.NET тоже может это делать, используя AspNetSynchronizationContext. Все это предоставляется фреймворком и автоматически устанавливается сантехникой из библиотеки классов. Они фиксируют контекст синхронизации в своем конструкторе и используют Begin / Invoke в своих методах Post и Send.

Когда вы пишете свой собственный SynchronizationContext, вы должны позаботиться об этих деталях. В вашем фрагменте вы не переопределяли Post и Send, но унаследовали базовые методы. Они ничего не знают и могут выполнить запрос только в произвольном потоке пула потоков. Итак, SynchronizationContext.Current теперь имеет значение null в этом потоке, поток пула потоков не знает, откуда пришел запрос.

Создать свой собственный не так уж и сложно, ConcurrentQueue и делегаты помогают значительно сократить код. Так поступили многие программисты, часто цитируют эту библиотеку. Но за это придется заплатить серьезную цену: этот цикл диспетчера в корне меняет поведение приложения в консольном режиме. Он блокирует нить до тех пор, пока петля не закончится. Как и Application.Run ().

Вам нужен совсем другой стиль программирования, знакомый вам по приложениям с графическим интерфейсом. Код не может длиться слишком долго, поскольку он закрывает цикл диспетчера, предотвращая отправку запросов на вызов. В приложении с графическим интерфейсом это заметно по тому, что пользовательский интерфейс перестает отвечать, в вашем примере кода вы заметите, что ваш метод медленно завершается, поскольку продолжение не может работать некоторое время. Вам нужен рабочий поток, чтобы выделять медленный код, бесплатного обеда нет.

Стоит отметить, почему этот материал существует. У приложений с графическим интерфейсом есть серьезная проблема, их библиотеки классов никогда не являются поточно-ориентированными, и их нельзя сделать безопасными с помощью lock. Единственный способ правильно их использовать - выполнять все вызовы из одного потока. InvalidOperationException, если вы этого не сделаете. Их цикл диспетчера поможет вам в этом, включив Begin / Invoke и async / await. Консоль не имеет этой проблемы, любой поток может что-то записать в консоль, и блокировка может помочь предотвратить смешивание их вывода. Таким образом, консольному приложению не требуется настраиваемый контекст синхронизации. YMMV.

person Hans Passant    schedule 07.10.2018
comment
Большое спасибо, Ганс .. отличный ответ - person KeyBored; 16.10.2018

Чтобы уточнить то, что уже было указано.

Класс SynchronizationContext, который вы используете в первом фрагменте кода, является реализацией по умолчанию, которая ничего не делает.

Во втором фрагменте кода вы создаете свой собственный MySC контекст. Но вам не хватает того элемента, который действительно заставил бы его работать:

public override void Post(SendOrPostCallback d, object state)
{
    base.Post(state2 => {
        // here we make the continuation run on the original context
        SetSynchronizationContext(this); 
        d(state2);
    }, state);        
    Console.WriteLine("Posted");
}
person felix-b    schedule 07.10.2018
comment
Но код после ожидания все еще продолжается в другом потоке, почему это происходит? - person Mohammed Noureldin; 10.07.2020

По умолчанию все потоки в консольных приложениях и службах Windows имеют только SynchronizationContext по умолчанию.

Пожалуйста, перейдите по ссылке https://msdn.microsoft.com/magazine/gg598924.aspx для более подробной информации. Здесь есть подробная информация о SynchronizationContext в различных типах приложений.

person Sham    schedule 07.10.2018