Многопоточное программирование может быть довольно сложным, и существует огромное количество концепций и инструментов, которые необходимо изучить, когда приступаешь к выполнению этой задачи. Чтобы помочь, Microsoft .NET Framework предоставляет класс SynchronizationContext. К сожалению, многие разработчики даже не знают об этом полезном инструменте.

Независимо от платформы — будь то ASP.NET, Windows Forms, Windows Presentation Foundation (WPF), Silverlight или другие — все .NET программы включают концепцию SynchronizationContext, и все программисты многопоточных решений могут извлечь выгоду из ее понимания и применения.

Необходимость в SynchronizationContext

Многопоточные программы существовали задолго до появления .NET Framework. Этим программам часто требовалось, чтобы один поток передавал «единицу работы»* другому потоку. Программы Windows были построены на основе концепции «цикл обработки сообщений», поэтому многие программисты использовали эту встроенную очередь сообщений для передачи единиц работы в цикл обработки. Каждая многопоточная программа, которая хотела использовать очередь сообщений Windows таким образом, должна была определить свое собственное пользовательское сообщение Windows и соглашение для его обработки.

<ред:* «единица работы» в исходном тексте «a unit of work» очень интересный термин который обозначает не только именованные и неименованные-лямбда функции, это может быть просто некоторый кусок кода-логики выделенный компилятором для передачи на исполнение в некотором контексте>

Когда .NET Framework был впервые опубликован, этот хорошо известный шаблон был стандартизирован. В то время единственным типом приложения с графическим интерфейсом, который поддерживался .NET, был Windows Forms. Однако разработчики платформы предвидели другие модели и разработали универсальное решение. Родился ISynchronizeInvoke.

Идея, лежащая в основе ISynchronizeInvoke, заключается в том, что “исходный” поток может поставить делегат* в очередь к “целевому” потоку, необязательно ожидая завершения выполнения этого делегата. ISynchronizeInvoke также предоставлял свойство для определения того, был ли текущий код сразу запущен на исполнение в целевом потоке; в этом случае постановка делегата в очередь была бы ненужной. Windows Forms предоставила единственную реализацию ISynchronizeInvoke, и был разработан шаблон для проектирования асинхронных компонентов, так что все были довольны.

<ред:* делегат – указатель на функцию именованную или анонимную или сформированную компилятором из какого-то куска кода или делегат в терминах C#>

Версия 2.0 .NET Framework содержала множество радикальных изменений. Одним из основных улучшений было внедрение асинхронных страниц в архитектуру ASP.NET. До .NET Framework 2.0 для каждого ASP.NET запроса требовался поток, пока запрос не будет завершен. Это было неэффективное использование потоков, поскольку создание веб-страницы часто зависит от запросов к базе данных и вызовов веб-служб, и потоку, обрабатывающему этот запрос, пришлось бы ждать завершения каждой из этих операций. С асинхронными страницами поток, обрабатывающий запрос, мог начинать каждую из операций <ред: из списка операций, составляющих запрос>*, а затем возвращаться обратно в пул потоков ASP.NET, когда операция завершались, другой поток из пула потоков ASP.NET <ред: выполнял следующую операцию из списка и в конце концов> завершал запрос.

<ред:* здесь дана настолько краткая формулировка принципа, можно сказать скомканная, что ее приходится уточнять>

Однако ISynchronizeInvoke плохо подходил для этой новой архитектуры ASP.NET асинхронных страниц. Асинхронные компоненты, разработанные с использованием шаблона ISynchronizeInvoke, не будут корректно работать в рамках ASP.NET страниц, поскольку ASP.NET асинхронные страницы связаны не с одним(не с единственным) потоком. Вместо постановки работы в очередь для исходного потока асинхронным страницам нужно только поддерживать количество незавершенных операций, чтобы определить, когда запрос страницы может быть завершен. После долгих размышлений и тщательного проектирования интерфейс ISynchronizeInvoke был заменен интерфейсом SynchronizationContext.

Концепция SynchronizationContext

ISynchronizeInvoke удовлетворял двум потребностям: определял, необходима ли синхронизация, и ставил в очередь единицу работы из одного потока в другой. SynchronizationContext был разработан для замены ISynchronizeInvoke, но по результатам процесса проектирования оказалось, что SynchronizationContext не является тем, чем был ISynchronizeInvoke.

Одной из особенностей SynchronizationContext является то, что он предоставляет способ поместить единицу работы в очередь контекста. Обратите внимание, что эта единица работы ставится в очередь контекста, а не конкретного потока. Это различие важно, поскольку многие реализации SynchronizationContext не основаны на одном конкретном потоке. SynchronizationContext не включает механизм определения необходимости синхронизации, поскольку это не всегда возможно.

Другим аспектом SynchronizationContext является то, что каждый поток имеет “текущий” контекст. Контекст потока не обязательно уникален; контекст конкретного потока может быть общим с другими потоками. Поток может изменить свой текущий контекст, но это довольно редкое явление или необходимость. <ред: то есть каждый поток принадлежит некоторому контексту, и одному контексту могут принадлежать несколько потоков. Таким образом, когда вы отдаете задание на выполнение в какой-то контекст, вы, в общем случае не знаете какой поток этого контекста будет исполнять это задание>

Третий аспект SynchronizationContext заключается в том, что он ведет подсчет незавершенных асинхронных операций. Это позволяет использовать ASP.NET асинхронные страницы и любой другой хост, нуждающийся в таком подсчете. В большинстве случаев количество таких незавершенных операций увеличивается при обращении к текущему SynchronizationContext, и уменьшается, когда соответствующий SynchronizationContext принимает уведомление о завершении операции из очереди контекста.

Существуют и другие аспекты SynchronizationContext, но они менее важны для большинства программистов. Наиболее важные аспекты проиллюстрированы на рисунке 1. (в исходнике используется слово рисунок, пусть будет рисунок)

Рисунок 1 Аспекты API SynchronizationContext

// The important aspects of the SynchronizationContext APIclass SynchronizationContext

{
// Dispatch work to the context.
void Post(..); // (asynchronously)
void Send(..); // (synchronously)
// Keep track of the number of asynchronous operations.
void OperationStarted();
void OperationCompleted();
// Each thread has a current context.
// If "Current" is null, then the thread's current context is
// "new SynchronizationContext()", by convention.
static SynchronizationContext Current { get; }
static void SetSynchronizationContext(SynchronizationContext);
}

Реализации SynchronizationContext

SynchronizationContext не имеет какой то одной заранее предопределенной реализации, это абстракция в чистом виде. Различные фреймворки и хосты могут свободно определять свою собственную реализацию контекста. Только через понимание этих различных реализаций и их ограничений можно понять, как концепция SynchronizationContext работает и чего не гарантирует. Я кратко рассмотрю некоторые из этих реализаций.

1.WindowsFormsSynchronizationContext(System.Windows.Forms.dll:System.Windows.Forms) Приложения Windows Forms создают и задают WindowsFormsSynchronizationContext в качестве текущего контекста для любого потока, который создает элементы управления пользовательским интерфейсом. Этот SynchronizationContext использует методы ISynchronizeInvoke в элементе управления пользовательского интерфейса, который передает делегаты главному циклу сообщений Win32. Контекст для WindowsFormsSynchronizationContext определяет единственный поток пользовательского интерфейса (UI поток).

Все делегаты, помещенные в очередь WindowsFormsSynchronizationContext, выполняются по одному; они выполняются определенным UI потоком в том порядке, в котором они были поставлены в очередь. Текущая реализация создает один WindowsFormsSynchronizationContext для каждого UI потока.

2. DispatcherSynchronizationContext (WindowsBase.dll: System.Windows.Threading) Приложения WPF и Silverlight используют DispatcherSynchronizationContext, который ставит делегатов в очередь к диспетчеру UI потока с “нормальным” приоритетом. Этот SynchronizationContext устанавливается в качестве текущего контекста, когда поток начинает свой цикл диспетчеризации, вызывая Dispatcher.Run. Контекст для DispatcherSynchronizationContext определяет единственный UI поток.

Все делегаты, помещенные в очередь DispatcherSynchronizationContext, выполняются по одному определенным UI потоком в том порядке, в котором они были поставлены в очередь. Текущая реализация создает один DispatcherSynchronizationContext для каждого окна верхнего уровня, даже если все они используют один и тот же базовый диспетчер.

3.Default SynchronizationContext (он же ThreadPool) (mscorlib.dll: System.Threading) Default SynchronizationContext – это созданный по умолчанию объект SynchronizationContext <ред: который всегда создается при запуске приложения, насколько я понимаю, соответственно, можно сказать, что это изначальный синх-контекст приложения>. По соглашению, если текущий SynchronizationContext потока равен null, то он неявно имеет SynchronizationContext по умолчанию.

Default SynchronizationContext помещает свои асинхронные делегаты в очередь ThreadPool, но выполняет свои синхронные делегаты непосредственно в вызывающем потоке. Следовательно, его контекст охватывает все потоки ThreadPool <ред: и текущий активный и любой который потенциально может быть задействован для выполнения асинхронного вызова, насколько я понимаю>, а также любой поток, который вызывает Send. Контекст “заимствует” потоки, вызывающие Send, помещая их в свой контекст до завершения выполнения делегата. В этом смысле, контекст по умолчанию может включать любой поток в процессе.

Default SynchronizationContext применяется к потокам ThreadPool, если код не размещен в ASP.NET. Default SynchronizationContext также неявно применяется к явным дочерним потокам (экземплярам класса Thread), если дочерний поток не устанавливает свой собственный SynchronizationContext. Таким образом, приложения пользовательского интерфейса обычно имеют два контекста синхронизации: UI SynchronizationContext, включающий поток пользовательского интерфейса, и SynchronizationContext по умолчанию, включающий потоки ThreadPool.

Многие асинхронные компоненты, основанные на событиях, не работают должным образом с SynchronizationContext по умолчанию. Печально известным примером является приложение пользовательского интерфейса, в котором один BackgroundWorker запускает другой BackgroundWorker. Каждый BackgroundWorker захватывает и использует SynchronizationContext потока, который вызывает RunWorkerAsync и позже выполняет свое событие RunWorkerCompleted в этом контексте. В случае одного BackgroundWorker обычно это SynchronizationContext на основе пользовательского интерфейса, поэтому RunWorkerCompleted выполняется в контексте пользовательского интерфейса, захваченном RunWorkerAsync (см. рисунок 2).

Рисунок 2. Одиночный BackgroundWorker в контексте пользовательского интерфейса
Рисунок 2. Одиночный BackgroundWorker в контексте пользовательского интерфейса

Однако, если BackgroundWorker запускает другой BackgroundWorker из своего обработчика DoWork, то вложенный BackgroundWorker не захватывает UI SynchronizationContext. DoWork выполняется потоком ThreadPool с SynchronizationContext по умолчанию. В этом случае вложенный RunWorkerAsync захватит SynchronizationContext по умолчанию, поэтому он выполнит свой RunWorkerCompleted в потоке ThreadPool вместо потока пользовательского интерфейса (см. рисунок 3).

Рисунок 3 Вложенные BackgroundWorkers в контексте пользовательского интерфейса
Рисунок 3 Вложенные BackgroundWorkers в контексте пользовательского интерфейса

По умолчанию все потоки в консольных приложениях и службах Windows имеют только Default SynchronizationContext. Это приводит к тому, что некоторые асинхронные компоненты, основанные на событиях, не могут работать в этой концепции. Одним из решений этой проблемы является создание явного дочернего потока и установка SynchronizationContext в этом потоке, который затем может предоставить контекст для таких проблемных компонент. Реализация SynchronizationContext выходит за рамки этой статьи, но класс ActionThread из библиотеки Nito.Async (nitoasync.codeplex.com) может использоваться в качестве реализации SynchronizationContext общего назначения.

AspNetSynchronizationContext (System.Web.dll: System.Web [internal class]) ASP.NET SynchronizationContext назначается потокам Тред-пула при выполнения ими кода страницы. Когда делегат помещается в очередь в контексте AspNetSynchronizationContext, он восстанавливает идентичность и культуру исходной страницы, а затем выполняет делегат напрямую. Делегат вызывается напрямую, даже если он “асинхронно” поставлен в очередь при вызове Post.

Концептуально контекст AspNetSynchronizationContext является сложным*<ред: сноска через три абзаца>. В течение жизненного цикла асинхронной страницы этот контекст применяется (подменяет исходный Default SynchronizationContext) к каждому потоку из ASP.NET пула потоков. После запуска асинхронных запросов контекст не включает никаких дополнительных потоков. По мере завершения асинхронных запросов потоки Тред-пула, выполняющие свои процедуры завершения, возвращаются в контекст: Default SynchronizationContext . Это могут быть те же потоки, которые инициировали запросы, но, скорее всего, это будут любые потоки, которые окажутся свободными на момент завершения операций.

Если несколько операций выполняются одновременно для одного и того же приложения, AspNetSynchronizationContext гарантирует, что они выполняются по одной за раз. Они могут выполняться в любом потоке, но этот поток будет иметь идентификатор и язык интерфейса исходной страницы. <ред: если что, я тоже не смог понять смысл этого абзаца, хочу отметить что абзац очень короткий.>

Одним из распространенных примеров является WebClient, используемый внутри асинхронной веб-страницы. DownloadDataAsync перехватит текущий SynchronizationContext и позже выполнит свое событие DownloadDataCompleted в этом контексте. Когда страница начнет выполняться, ASP.NET выделит один из своих потоков для выполнения кода на этой странице. Страница может вызвать DownloadDataAsync, который сразу выполнится и завершится; ASP.NET ведет подсчет незавершенных асинхронных операций, поэтому понимает, что страница не завершена. Когда объект WebClient загрузит запрошенные данные, он получит уведомление в потоке Тред-пула. Этот поток вызовет DownloadDataCompleted в перехваченном контексте. Контекст останется в том же потоке, но обеспечит запуск обработчика событий с правильной идентификацией и культурой.

<ред:* действительно как-то очень сложно, я бы даже сказал неоднозначно написано про реализацию контекста AspNetSynchronizationContext, скорее всего это тоже по причине того, что через чур много информации попытались передать всего в трех абзацах, надо искать более подробные пояснения, по моему.>

Замечания по реализациям SynchronizationContext

SynchronizationContext предоставляет средства для написания компонент, которые могут работать в разных фреймворках. BackgroundWorker и WebClient - это два примера, которые одинаково хорошо работают в Windows Forms, WPF, Silverlight, консоли и ASP.NET приложениях. Однако есть некоторые моменты, которые необходимо иметь в виду при проектировании таких повторно используемых компонентов.

Вообще говоря, реализации SynchronizationContext нельзя сравнивать напрямую. Это означает, что нет эквивалента ISynchronizeInvoke.InvokeRequired. Однако это не является каким-то недостатком; на самом деле код становится чище и его легче проверить, если он всегда выполняется в одном известном контексте, вместо того чтобы пытаться писать код который поддерживает несколько возможных контекстов.

Не все реализации SynchronizationContext гарантируют порядок выполнения делегатов или синхронизацию делегатов. Реализации SynchronizationContext на основе пользовательского интерфейса удовлетворяют этим условиям, но ASP.NET SynchronizationContext обеспечивает только синхронизацию. SynchronizationContext по умолчанию не гарантирует ни порядок выполнения, ни синхронизацию.

Не существует соответствия 1:1 между экземплярами SynchronizationContext и потоками. WindowsFormsSynchronizationContext имеет сопоставление 1:1 с потоком (при условии, что SynchronizationContext.CreateCopy не вызывается), но это не относится ни к одной из других реализаций. В общем, лучше не предполагать, что какой-либо экземпляр контекста будет запущен в каком-либо конкретном потоке.

Наконец, SynchronizationContext.Post метод не обязательно является асинхронным. Большинство реализаций реализуют его асинхронно, но AspNetSynchronizationContext является выдающимся исключением. Этот факт может вызвать неожиданные проблемы с повторным входом <ред: повторный вход = “stack dives” или «рекурсия» в других работах>. Краткое описание этих различных реализаций можно увидеть на рисунке 4.

Рисунок 4 Краткое описание реализаций SynchronizationContext
Рисунок 4 Краткое описание реализаций SynchronizationContext

AsyncOperationManager и AsyncOperation

Классы AsyncOperationManager и AsyncOperation в .NET Framework являются облегченными оболочками вокруг абстракции SynchronizationContext. AsyncOperationManager фиксирует текущий SynchronizationContext при первом создании AsyncOperation, заменяя SynchronizationContext по умолчанию, если текущее значение равно null. AsyncOperation асинхронно отправляет делегаты в захваченный SynchronizationContext.

Большинство асинхронных компонентов, основанных на событиях, используют в своей реализации AsyncOperationManager и AsyncOperation. Они хорошо работают для асинхронных операций, которые имеют определенную точку завершения — то есть асинхронная операция начинается в одной точке и заканчивается событием в другой. Другие асинхронные уведомления могут не иметь определенной точки завершения; это может быть тип подписки, который начинается в какой-то момент и затем продолжается бесконечно. Для этих типов операций SynchronizationContext может быть захвачен и использован напрямую.

Новые компоненты не должны использовать асинхронный шаблон на основе событий. The Visual Studio asynchronous Community Technology Preview (CTP) включает документ, описывающий шаблон асинхронных решений на основе задач, в котором компоненты возвращают объекты Task и Task<TResult> вместо создания событий через SynchronizationContext. API-интерфейсы, основанные на задачах, — это будущее асинхронного программирования в .NET.

Примеры поддержки SynchronizationContext из библиотек

Простые компоненты, такие как BackgroundWorker и WebClient, сами по себе неявно переносимы, потому что они скрывают захват и использование SynchronizationContext. Многие библиотеки имеют более заметное использование SynchronizationContext. Предоставляя API-интерфейсы с использованием SynchronizationContext, библиотеки не только получают независимость от фреймворка, они также обеспечивают точку расширения для продвинутых конечных пользователей.

В дополнение к библиотекам, которые я сейчас рассмотрю, текущий SynchronizationContext считается частью ExecutionContext. Любая система, которая фиксирует ExecutionContext потока, фиксирует текущий SynchronizationContext. Когда ExecutionContext восстанавливается, SynchronizationContext обычно тоже восстанавливается.

Windows Communication Foundation (WCF):UseSynchronizationContext WCF имеет два атрибута, которые используются для настройки поведения сервера и клиента: ServiceBehaviorAttribute и CallbackBehaviorAttribute. Оба этих атрибута имеют свойство типа Bool: UseSynchronizationContext. Значение этого атрибута по умолчанию равно true, что означает, что текущий SynchronizationContext захватывается при создании канала связи, и этот захваченный SynchronizationContext используется для постановки в очередь методов контракта.

Обычно такое поведение является именно тем, что необходимо: серверы используют SynchronizationContext по умолчанию, а обратные вызовы клиента используют соответствующий SynchronizationContext пользовательского интерфейса. Однако это может вызвать проблемы, когда требуется повторный вход (ограниченная рекурсия), например, когда клиент вызывает серверный метод, который вызывает обратный вызов клиента. В этом и подобных случаях автоматическое использование WCF SynchronizationContext может быть отключено путем установки UseSynchronizationContext в значение false.

Это всего лишь краткое описание того, как WCF использует SynchronizationContext. Более подробную информацию смотрите в статье “Контексты синхронизации в WCF” (msdn.microsoft.com/magazine/cc163321) в ноябрьском номере журнала MSDN за 2007 год.

Windows Workflow Foundation (WF): WorkflowInstance.SynchronizationContext Хосты WF изначально использовали WorkflowSchedulerService и производные типы для управления планированием действий рабочего процесса в потоках. Часть обновления .NET Framework 4 включала свойство SynchronizationContext для класса WorkflowInstance и его производного класса WorkflowApplication.

SynchronizationContext может быть установлен напрямую, если процесс хостинга создает свой собственный WorkflowInstance. SynchronizationContext также используется WorkflowInvoker.InvokeAsync, который захватывает текущий SynchronizationContext и передает его своему внутреннему WorkflowApplication. Этот SynchronizationContext затем используется для публикации события завершения рабочего процесса, а также для отложенного вызова workflow активностей.

Task Parallel Library (TPL): TaskScheduler.FromCurrentSynchronizationContext and CancellationToken.Register TPL использует объекты task в качестве своих единиц работы
и выполняет их через TaskScheduler. Дефолтный TaskScheduler действует как default
SynchronizationContext по умолчанию, помещая задачи в очередь ThreadPool. Существует другой TaskScheduler, предоставляемый TPL, который помещает задачи в очередь SynchronizationContext. Нотификация об изменениях в пользовательском интерфейсе может быть реализована с помощью вложенной задачи, как показано на рисунке 5.

Рисунок 5 Нотификация об изменениях в пользовательском интерфейсе

private void button1_Click(object sender, EventArgs e)
{
    // This TaskScheduler captures SynchronizationContext.Current.
    TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    // Start a new task (this uses the default TaskScheduler, 
    // so it will run on a ThreadPool thread).
    Task.Factory.StartNew(() =>
    {
        // We are running on a ThreadPool thread here.
        ; // Do some work.
        // Report progress to the UI.
        Task reportProgressTask = Task.Factory.StartNew(() =>
        {
            // We are running on the UI thread here.
            ; // Update the UI with our progress.
        },
          CancellationToken.None,
          TaskCreationOptions.None,
          taskScheduler);
        reportProgressTask.Wait();
        ; // Do more work.
    });
}

Класс CancellationToken используется для любого типа принудительного завершения (далее «прерывания») в .NET Framework 4. Для интеграции с существующими способами прерывания этот класс позволяет зарегистрировать делегат для вызова при запросе прерывания. Когда делегат зарегистрирован, SynchronizationContext может быть передан. Когда запрашивается прерывание, CancellationToken ставит делегат в очередь SynchronizationContext вместо того, чтобы выполнять его напрямую.

Microsoft Reactive Extensions (Rx): observeOn, subscribeOn и SynchronizationContextScheduler Rx — это библиотека, которая обрабатывает события как потоки данных. Оператор ObserveOn ставит события в очередь через SynchronizationContext, а оператор SubscribeOn ставит в очередь подписки(subscriptions) на эти события через SynchronizationContext. ObserveOn обычно используется для обновления пользовательского интерфейса входящими событиями, а SubscribeOn используется для обработки событий из объектов пользовательского интерфейса.

Rx также имеет свой собственный способ постановки единиц работы в очередь: интерфейс IScheduler. Rx включает SynchronizationContextScheduler, реализацию IScheduler, которая занимается формированием очереди для SynchronizationContext.

 Visual Studio Async CTP: await, ConfigureAwait, switchTo и EventProgress<T> Поддержка Visual Studio асинхронных преобразований кода была анонсирована на конференции Microsoft Professional Developers Conference 2010. По умолчанию текущий SynchronizationContext захватывается в точке с await , и этот SynchronizationContext используется для возобновления после await  (точнее, он захватывает текущий SynchronizationContext, если он не равен null, и в этом случае он захватывает текущий TaskScheduler):

private async void button1_Click(object sender, EventArgs e)
{
    // SynchronizationContext.Current is implicitly captured by await.
    var data = await webClient.DownloadStringTaskAsync(uri);
    // At this point, the captured SynchronizationContext was used to resume
    // execution, so we can freely update UI objects.
}

ConfigureAwait предоставляет средство избежать поведения с захватом default SynchronizationContext; передача значения false для параметра flowContext предотвращает использование SynchronizationContext чтобы можно было возобновить выполнение после инструкции await. Существует также метод расширения для экземпляров SynchronizationContext, называемый switchTo; это позволяет любому асинхронному методу переключаться на другой SynchronizationContext, вызывая switchTo и ожидая результата.

Асинхронный CTP вводит общий шаблон для получения информации о ходе выполнения асинхронных операций: интерфейс IProgress<T> и его реализация EventProgress<T>. Этот класс захватывает текущий SynchronizationContext при его создании и вызывает событие ProgressChanged в этом контексте.

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

Ограничения и гарантии

Понимание SynchronizationContext полезно для любого программиста. Существующие компоненты, предназначенные для нескольких разных фреймворков, используют его для синхронизации своих событий. Библиотеки могут предоставлять его для обеспечения повышенной гибкости. Опытный программист, который понимает ограничения и гарантии SynchronizationContext, лучше справляется с написанием и использованием классов для таких компонент.

<ред: в исходном тексте в этом месте вы найдете абзац информации об авторе - Stephen Cleary, и, в конце, строчку благодарности Eric Eilebrecht-у за ревью>

<ред: С Наступающим Новым Годом! Счастья, удачи, интересных проектов!

Сёргий>

Комментарии (20)


  1. MonkAlex
    30.12.2023 08:12
    +2

    Статья 2011 года нынче не так актуальна.

    Из перечисленных технологий ( ASP.NET, Windows Forms, Windows Presentation Foundation (WPF), Silverlight ) всё уже в состоянии "никому не нужно".

    В асп.нет кор от необходимости следить за контекстом синхронизации ушли, насколько я знаю, т.к. по умолчанию начали использовать тред-пул и синхронизировать нечего и незачем. Т.е. можно не думать о синхронизации, задедлочиться случайно довольно сложно.

    Винформы актуальны только для миграций легаси на свежие фрейморки.

    WPF примерно так же. Если кто-то пользуется, то понимать контекст синхронизации важно, но это важно уже сколько, лет 10?

    И сильверлайт тоже умер на текущий момент.

    Т.е. в реальности актуально сейчас только для десктопов WinForms и WPF. В 2024 наступающем не то чтобы самые актуальные технологии.


    1. rukhi7 Автор
      30.12.2023 08:12

      Я думаю Stephen Toub, который написал в начале этого-23-го года Пост: How Async/Await Really Works in C# с Вами бы не согласится, так как он пишет что:

      However, it did add one notable advance that the APM pattern didn’t factor in at all, and that has endured into the models we embrace today: SynchronizationContext.

      что, в контексте нашей дискуссии, можно перевести как:

      <теперь у нас есть> одно заметное усовершенствование, которое шаблон APM вообще не учитывал, и которое сохранило свою ценность в моделях, которые мы используем сегодня: SynchronizationContext.

      Это "сегодня" было датировано March 16th, 2023 .

      Но возможно Вы знаете про какую-то более современную замену для SynchronizationContext? Поделитесь секретом? Просто я сомневаюсь, что такую фундаментальную концепцию как SynchronizationContext можно чем-то заменить, поэтому Ваш секрет был бы настоящим открытием для меня.

      Потом, мне кажется, что .NET и даже C# в каком-то смысле, тоже технологии.


      1. MonkAlex
        30.12.2023 08:12

        Формально SynchronizationContext всё ещё есть и работает, бесспорно. Но по факту, его учитывать стоит только в десктопных приложениях, которые на C# пишут достаточно редко. Спрос на такую разрботку не очень высокий.

        ПС: и возможно в каких-то ещё UI приложениях, которые синхронизируют отрисовку силами одного треда, например https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs


        1. iamanatoly
          30.12.2023 08:12

          Если не секрет, а что пользуется спросом в сфере десктоп разработки сейчас?

          Мне казалось, C# там должен быть среди первых


          1. MonkAlex
            30.12.2023 08:12

            А нет в десктопе жизни. Если кто-то и пишет, то пишут обычно сразу кроссплатформ, что-то из веб-фреймворков, для десктопа заворачивают в электрон.

            В C# из коробки кроссплатформа нет, существуют сторонние фреймворки, из которых я знаю разве что AvaloniaUI, но у меня сомнения, насколько он необходим. Веб писать и поддерживать возможно нынче легче.


        1. hebedombiu
          30.12.2023 08:12

          Не только. Я бэк интегрировал с Epic Online Services. Они требуют чтобы работа с апи была в единственном потоке. Плюс обёртка над их коллбеками для async/await. Пришлось писать свой SynchronizationContext (реализация sc для одного потока из vs не подошла)


  1. itGuevara
    30.12.2023 08:12

    Параллельные вычисления — Все дело в контексте-синхронизации (SynchronizationContext)

    Больше интересует не программная реализация синхронизации (параллелизма), а формальная реализация параллелизма и конкуренции: синхронизация на схеме и математическим языком.

    Есть классический пример описания параллелизма и конкуренции в алгебре процессов: одна кофе машина и два ученых. Два ученых бросают монеты не по очереди, а на конкурентной основе (случайно). Берет кофе тот, кто первым успеет и не обязательно, что он за этот кофе заплатил. После публикации статьи кофе машина готова принимать монеты.

    Пример: An Introduction to Milner’s CCS

    Есть ли более подробное описание этого примера (как в CCS \ LTS, так и на других языках)? Вообще, как в виде схем формализуются подобные алгоритмы? Схемы по ссылке не передают замысел сценария (синхронизация) в достаточном объеме.  


    1. MonkAlex
      30.12.2023 08:12

      Статья не про это, насколько я понимаю.

      SynchronizationContext в дотнете - про контекст, в котором надо продолжить работу после выполнения асинхронной (в том числе возможно выполнения асинхронной на другом треде).

      Т.е. речь про условную кофе машину, которая как раз запомнила (за счёт контекста), кто кинул монету, сварила асинхронно кофе, и вернула кофе именно тому, кто просил (вернула по контексту).

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


      1. rukhi7 Автор
        30.12.2023 08:12

        SynchronizationContext в дотнете - про контекст, в котором надо продолжить работу после выполнения асинхронной

        Насколько я понял, SynchronizationContext - про контекст который определяет как и/или где (в каком потоке) выполнять асинхронную операцию.


        1. MonkAlex
          30.12.2023 08:12

          Вам ещё в прошлой статье сказали - нет у асинхронной операции потока.

          Есть поток до асинхронщины, есть после. Сама асинхронность - это тот факт, что пока работа выполняется вне CPU (диск или сеть), поток может быть занят любой другой полезной работой.

          И SynchronizationContext - это механизм, который для WPF позволяет выполнить работу после асинхронщины снова на UI-потоке (если вам это нужно).


          1. rukhi7 Автор
            30.12.2023 08:12

            Вам ещё в прошлой статье сказали - нет у асинхронной операции потока.

            А если я запускаю какой-то свой, собственноручно написанный, сложный-тяжелый алгоритм расчета, которому больше негде выполняться как на CPU? Запускаю этот алгоритм-расчеты как асинхронную операцию, для этой асинхронной операции будет создан дополнительный поток, чего бы мне кто не говорил в прошлой статье, уж извините, говорить можно что угодно, но еще надо чтобы программа работала.


            1. MonkAlex
              30.12.2023 08:12

              Код покажите, обсудим, где какой поток работает. Да, если работа на CPU - поток будет. Но если вы не создадите Thread\Task самостоятельно, то вся работа синхронно будет выполнена на вызывающем треде.


              1. rukhi7 Автор
                30.12.2023 08:12

                ну вот есть пример, все из той же статьи  How Async/Await Really Works in C# :

                using System.Diagnostics;
                
                Time(async () =>
                {
                    Console.WriteLine("Enter");
                    await Task.Delay(TimeSpan.FromSeconds(10));
                    Console.WriteLine("Exit");
                });
                
                static void Time(Action action)
                {
                    Console.WriteLine("Timing...");
                    Stopwatch sw = Stopwatch.StartNew();
                    action();
                    Console.WriteLine($"...done timing: {sw.Elapsed}");
                }

                здесь для вызова action();

                будет создан (взят из ТредПула) новый поток в консольном приложении, потому что она помечена как async, там далее автор показывает как подменить SynchronizationContext чтобы изменить это поведение с выделением потока. Чтобы вызывающий поток все таки дождался завершения асинхронной операции, насколько я понимаю в измененном примере поток действительно не создается. Пример как раз показывает как управлять тем будет создаваться поток для вызова функции помеченной async или нет с помощью SynchronizationContext, насколько я понимаю.

                Интересно, что этот пример написан, как мне кажется, как будто специально в такой манере что-бы его поняли как можно МЕНЬше читателей! Там же вызов засунули внутрь другого вызова, надо очень внимательно вникать.


                1. propell-ant
                  30.12.2023 08:12

                  Может лучше другой пример возьмете (там же много их). А то этот про то как не надо использовать async void.


                1. MonkAlex
                  30.12.2023 08:12

                  Смотрите.

                  Timing... и Enter будут выполены основным (первым) тредом, синхронно, без всякой "магии".

                  Дальше у нас вызов await Task.Delay(TimeSpan.FromSeconds(10)); который имитирует асинхронную работу. Если это реальная асинхронная работа (например если бы тут был запрос по сети), то CPU в этот момент будет свободен и даже первый тред может выполнять другую полезную работу, если она в приложении имеется. Конкретно Task.Delay насколько я помню опирается на планировщик и таймер системный, т.е. тоже будет действительно асинхронным. async-await и стоящая за ними стейт-машина сохранит SynchronizationContext (который в консольном приложении по умолчанию использует тред пул) и продолжит выполнение спустя 10 секунд на случайном потоке с тред пула. Это может быть как всё ещё наш первый тред, так и любой другой, который в пуле есть и не занят.

                  Т.е. записи ...done timing и Exit будут выполнены случайным потоком из тред пула, возможно первым же, возможно "вторым".

                  И это всё. У нас есть тред, который пишет первые записи в консоль до выполнения асинхронного вызова Task.Delay и есть тред, который делает эту работу после вызова.

                  Сама задержка не требует работы тредов в дотнете - среда выполнения возьмет на себя работу по взаимодействию с ОС и её таймерами (что нужно проснуться через 10 секунд), 10 секунд никакой тред не будет молотить CPU в ожидании, а потом служебный тред будет поднят со стороны ОС, чтобы отработать после 10 секунд таймера. Да, в схеме есть служебные треды, но они выполняют небольшую служебную работу, они не молотят 10 секунд ожидая.

                  UPD: SynchronizationContext во всей этой схеме нужен только для одного - понять, где стоит продолжить выполнение после await-инструкции. Если явно не сказано ConfigureAwait(false), то будет попытка захвата контекста и именно в него будет передано продолжение выполнения. Контекст по умолчанию (консольный) возьмет тред с тред-пула, а контекст WPF например положит выполнение на диспатчер UI треда.


                  1. rukhi7 Автор
                    30.12.2023 08:12

                    Я проверил с помощью ManagedThreadId, действительно везде тот же самый поток, то есть поток не создается!

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

                    Спасибо за критику!


                    1. alexalok
                      30.12.2023 08:12

                      В тредпуле (если он отдельно не настроен) есть несколько заранее созданных потоков, поэтому в общем случае новый поток создаваться действительно не будет.

                      Чтобы добиться желаемого поведения, то есть "смены" потока, можно сделать так, чтобы исходный был 100% занят во время выполнения продолжения асинхронной операции. Этого можно добиться, например, введя его в бесконечный цикл сразу после

                      Console.WriteLine($"...done timing: {sw.Elapsed}");


      1. itGuevara
        30.12.2023 08:12

        Т.е. речь про условную кофе машину, которая как раз запомнила (за счёт контекста), кто кинул монету, сварила асинхронно кофе, и вернула кофе именно тому, кто просил (вернула по контексту).

        Там много вариантов, в том числе и такой. Если не читать, то посмотреть можно:

        Smart Contract formal verification: Process Calculus and Modal Logics, on 4 July 2018

        Вопрос: как любой сценарий с параллелизмом, конкуренцией, бисимуляцией формализовывать просто и понятно (математикой и графически) и чтобы графически была видна такая взаимосвязь. Через состояния системы - не особо хорошо видно.


        1. MonkAlex
          30.12.2023 08:12

          У меня ощущение, что вы статьей промахнулись. Всё что вы спрашиваете, никак не относится к теме. Или я совсем не понимаю, о чём вы.


    1. rukhi7 Автор
      30.12.2023 08:12

      Я могу Вам предложить пару вариантов схем:

      Многопоточность (Multithreading) для практического программирования. То, о чем «забыть-нельзя-вспоминать» придется

      или, вот здесь можно найти несколько схем:

      Можно ли решить задачу реального времени без RTOS, разберем реализованную задачу

      в параграфе: Временные диаграммы, задача планировщика операций

      , наверно не совсем по теме, но мне кажется это в каком-то смысле решение родственной задачи.