Pull to refresh

Comments 45

Предположим у вас есть два потока, t1 и t2. И t1 исполняет некоторую работу, и в какой-то момент желает передать выполнение кода потоку t2.

Зачем это делать кроме UI?
Ну если отойти от темы статьи, то вот пример, один поток выполняет прослушку сетевого протокола, и в момент подключения передает обработку другому потоку. Т.е. многопоточный клиент-сервер описывается данным предложением.
Так же можно подвести очереди сигналов и сообщений, ну и вообще большое количество задач, где множество потоков могут писать, а один разгребает очередь.
Это прекрасно описывается формулировкой «ставит задачу», «передает сообщение». Прямой передачи выполнения кода лучше избегать. И особенно полезно избегать ситуации, когда во втором потоке зачем-то используется именно синхронизированная обработка.
| информации об этом новом класса в .NET Framework, действительно немного

Новом? Ему сто лет в обед.

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

абсолютно не согласен! SynchronizationContext — очень важная вещь в инфраструктуре .net, да и вообще если вы делаете что-то сложнее чем «hello, world!» для многопоточных приложений.

WCF, WF и т.д. — лишь примеры использования. Если есть message loop — то SynchronizationContext подходит как ни что другое.

очень хороший пост на эту тему — msdn magazine.
Есть подозрение, что товарищ aush не пользуется контекстом синхронизации, пользуясь асинхронным программированием и не зная, что он там есть (в чём большого ужаса я не вижу).

В статье, что Вы привели (она от 2011 года, тут прямо не статья, а машина времени) внизу всё расписано о «ожидающихся» async/await и как они связаны с контекстом.

На мой взгляд, вся польза от знаний о SynchronizationContext свелась к пониманию того, что не надо больше писать, используя await.
Не обратил внимания, что это перевод старой статьи. Конечно, я говорил именно о том, что сейчас нет причин лезть на этот уровень, т.к. имеющиеся высокоуровневые абстракции обеспечивают весь необходимый функционал.
Что за бред. Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.
SynchronizationContext это элементарная и совершенно базовая штука. Для реализации того же ActiveObject очень удобно
Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.

aush, как мне кажется, пишет о том, что в большей части случаев достаточно просто воспользоваться async/await, и предоставить компилятору и BCL думать о синхронизации и циклах. Повышение уровня абстракции.
Так await не будет работать в консольном приложении если не подумать о SynchronizationContext и не создать loop
Что значит «не будет работать»? Просто каждый следующий кусок кода будет в произвольном потоке из тредпула.

Is it a problem then that using async like this in a console app might end up running continuations on ThreadPool threads? I can’t answer that, because the answer is entirely up to what kind of semantics you need in your application. For many applications, this will be perfectly reasonable behavior.

blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx
Для того поведения что вы описали есть .ConfigureAwait(false)
Спасибо, я в курсе. Более того, я в курсе, что это рекомендуемое поведение для библиотечного кода. Как это влияет на тот факт, что можно написать консольное приложение с async/await, не используя никакой контекст синхронизации?

(если честно, я вообще не уверен, что захват контекста синхронизации по умолчанию — это благо, особенно с тех пор, как я малость задолбался, отслеживая, что везде, где у меня используется await, проставлено «работать без синхронизации»)
Получить race condition становится элементарно в коде который на глаз (при отсутствии упоминания что выполнение идет на потоке без контекста) выглядит как абсолютно корретный и несодержащий проблем мультипоточной мутации.

Если вас смутила моя фраза «не будет работать», я перефразирую. Скорее всего будет работать неочевидно некорректно.
Получить race condition становится элементарно

Race condition между чем и чем, простите?

Скорее всего будет работать неочевидно некорректно.

Скажите мне, как именно «неочевидно некорректно» может работать код вида

async Do()
{
    await Part1();
    await Part2();
    await Part3();
    await Part4();
    await Part5();
}
Вы реально не понимаете или занимаетесь жирным троллингом?

Если троллите — переметнитесь на других пожалуйста.
Если не понимаете — вот код который падает без контекста.

 class Program
    {
        private static List<object> list = new List<object>();

        static void Main(string[] args)
        {
            Run1();
            Run2();

            Console.ReadLine();
        }

        private static async void Run2()
        {
            while (true)
            {
                await Task.Yield();

                list.Add(new object());
            }
        }

        private static async void Run1()
        {
            while (true)
            {
                await Task.Yield();
                foreach (var l in list)
                {
                }
            }
        }
    }


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

В общем я хотел просто сказать что понимать что такое синк контекст необходимо. И никакие async/await не скроют то что под ними, потому как нижележащий слой очень сильно на них влияет.
Это какой-то очень сильно вымышленный пример. Я не очень понимаю, что и зачем он должен делать. Вы хотите, чтобы методы выполнялись последовательно? Сделайте их async Task и .Wait() на каждом вызове. Вы хотите, чтобы они выполнялись параллельно? Все равно нужно синхронизировать доступ к list.

async void — это вещь, которой надо очень сильно избегать.
Избегайте. Я же не испытываю никаких проблем с async void.

Пример такой чтобы вы могли скопировать, запустить и убедиться.
Если хотите более реального — представьте себе game loop без vsync который постоянно делает update/render. Вы находясь на потоке лупа запустили Task с созданием тяжелого объекта и сделали на нем await, в надежде потом воткнуть этот объект в иерархию сцены.

Если бы контекст был — вернулись бы в поток лупа, и как следствие втыкали бы между апдейтами.
Когда контекста нет — втыкаете на BG потоке, и вполне вероятно что сейчас на потоке лупа идет обход сцены и вы воткнете в коллекцию по которой топает итератор.

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

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

Ну и да, рекомендация делать ConfigureAwait(false) в максимально возможном числе мест — она не просто так появилась, контекст влечет за собой накладные расходы (а иногда — и дедлоки).
Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.
Это с какой стати? Если отсутствует текущий контекст синхронизации, continuation будет вызвано в потоке из пула. Другое дело, что к тому моменту может завершиться главный поток вместе со всем приложением, но к работоспособности async/await это не имеет ни малейшего отношения.
В котором Вы за 6 сообщений с помощью надуманного примера показали, что List из двух тредов одновременно модифицировать и читать нельзя без блокировок? Не очень полезное времяпрепровождение.
Все же «не сможете использовать» и «должны знать особенности реализации» (что в консоли остаток метода реально выполнит другой тред) это разные вещи.
Специально для тебя неумеющего читать, ненадуманный пример: https://habrahabr.ru/post/232169/#comment_7873927

Про разные вещи (последний абзац):
https://habrahabr.ru/post/232169/#comment_7873817

И в целом — https://www.youtube.com/watch?v=DgdP5U28jHc
А какие у него, собственно, альтернативы, позволяющие запускать фоновые задачи?

BackgroundWorker — хороший инструмент, но только до тех пор, пока вы не запутаетесь в огромном количестве этих воркеров… Кроме того, он зачастую вынуждает писать логику в классе формы, чего в крупных проектах лучше не делать.

Задачи (Task)? Да, задачи — это круто, а асинхронные методы — еще круче. До тех пор, пока в требованиях к приложению не значится совместимость со вторым фреймворком.

Control.Invoke? Этот метод вылетает при закрытии формы. Видели приложения, которые не могут закрыться без ошибки? В половине случаев тут виноват Control.Invoke.
А какие у него, собственно, альтернативы, позволяющие запускать фоновые задачи?

Тредпул? Автономный тред с очередью задач?

До тех пор, пока в требованиях к приложению не значится совместимость со вторым фреймворком.

К счастью, сейчас такие требования все-таки редкость.
Тредпул? Автономный тред с очередью задач?
И как же тредпул решает проблему возвращения в поток UI после выполнения фоновой задачи? Уйти в фоновой поток действительно просто, я же перечислял способы отобразить результат вычислений на форме.
И как же тредпул решает проблему возвращения в поток UI после выполнения фоновой задачи?

Никак не решает.

Уйти в фоновой поток действительно просто, я же перечислял способы отобразить результат вычислений на форме.

Вы спрашивали альтернативы, позволяющие запустить фоновые задачи, а не «запустить фоновую задачу и отобразить результат на форме». Для взаимодействия с UI разумных альтернатив контексту синхронизации нет — хотя бы потому, что большая часть из перечисленного вами внутри себя работает через него же.
Ну, я как-то неявно предполагал, что результат выполнения любой задачи должен быть как-то использован, а не позабыт :)
Во-первых, бывают не-UI-ные приложения. Во-вторых, бывают задачи, результат которых не потребляется напрямую UI. И, наконец, в-третьих, есть больше одного способа передать результат в UI, не обязательно напрямую в поток ходить.
Отображение на UI не подразумевает маршалинг в UI поток. Можно без проблем написать UI фреймворк в котором все взаимодействие с контролами будет thread-safe. И не только можно, а и пишут, и используют. Или же просто помнят какие вещи надо маршалить, а какие нет.

На моем текущем проекте заменить текстовому полю текст можно безопасно из BG потока, причем даже синхронизаций внутри UI либы не написано никаких. Все из за того что присвоение ссылки в дотнете атомарно, а логика не обращается внутри логической еденицы к полю 2 раза.
Этот способ не будет работать в 2012й студии, потому что компилятору не хватит еще десятка методов. А если взять их реализацию из BCL, то компилятор из 2010й студии начнет выдавать некорректный код. Но если решить эту проблему, то асинхронные методы действительно заработают.
Прекрасно работает в 2012 и 2013 студиях.
Ну, значит вы пропустили в своей статье кучу интересного. Потому что в вашей MonoLib я ясно вижу следы внутренних интерфейсов из 4.5 фреймворка, которые совершенно не нужны компилятору из 2010й студии, и в AsyncCTP их нет.

Я не верю, что вы не проверяли этот код в 2012й студии :)
У меня этот код уже полтора года в продакшне и пережил переезд проекта сначала на 2012, затем на 2013. И я всё ещё не могу понять логики, по которой он не должен работать.
В AsyncCTP не было, к примеру, интерфейса IAsyncStateMachine, и никаких намеков, что он будет нужен. Если бы библиотека делалась изначально под 2010ю студию (как аналогичная моя) — то в 2012й студии код упал бы с ошибкой компиляции из-за ненайденного интерфейса и нескольких методов.

С другой стороны, если взять библиотеку BCL (Backward Compatibility Library) — то в ней задача (Task, точнее TaskCompletionSource) создается по первому требованию, а не при создании AsyncTaskMethodBuilder. Смысл этого я не понимаю, если честно. Но при попытке использовать старый компилятор все асинхронные методы сразу же зависают, потому что задача создается уже после упаковки структуры (как результат — создаются две задачи, одна рабочая, а вторая возвращается).

PS более всего меня удивило, что в ваша библиотека работает, несмотря на отсутствие интерфейса IAsyncMethodBuilder и его метода PreBoxInitialization в билдерах. Видимо, компилятор несколько умнее, чем мне думалось…
UPD сам удивился — сам разобрался. В коде от Microsoft интерфейс IAsyncMethodBuilder — полностью внутренний, компилятор про него не знает. Он нужен в случае, если компилятор забудет вызвать метод SetStateMachine. Однако, компилятор почему-то этого никогда не забывает :)
Простите-извините, комментировать статью 2008 года в 2014 — это интересно.

В .NET я нашел два класса реализующих контекст синхронизации для пользовательского интерфейса, один для WinForms и один для WPF.


Сейчас контекст синхронизации неявно используется в асинхронном программировании (Task + async/await) и не зависит от WinForms / WPF / etc.

Но если хочется явного SynchronizationContext можно сделать, например, так:

// Suppose we are on a UI thread in a Windows Forms / WPF application:
_uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Run (() => Foo())
     .ContinueWith (ant => lblResult.Content = ant.Result, _uiScheduler);

Немного веселой магии:

await Task.Delay(TimeSpan.FromTicks(9999));
await Task.Delay(TimeSpan.FromTicks(10000));

При вызове с UI потока какого-нибудь WPF или WinForms SynchronizationContext::Post будет вызван только для второго случая (с 10000) :-)
А что тогда будет вызвано для первого случая?.. Когда я декомпилировал реализацию await, логика выглядела довольно простой, и без всякой магии.
Ничего, выполнится синхронно. Всё из-за того, что после деления на TicksPerMiliseconds и прямого каста к инту из 9999 тиков получается 0мс. А 0мс — это вернуть управление синхронно (return CompletedTask) — просто особенности реализации Task.Delay.
И первое впечатление от найденных статей по контексту синхронизации, это то что нужно. Но после более детального изучения стало понятно что SynchronizationContext появился и развивался в рамках задачи взаимодействия с UI, и этими задачи его использование и ограничивается. Собственно в самом FCL всего два класса от него наследуются, один для WPF и второй для WinForms.

Ну я не согласен: AspNetSynchronizationContextBase, WorkflowSynchronizationContext, ComPlusSynchronizationContext… Их довольно много только в стандартной поставке, не считая кастомных.
Не знаю, имеет ли смысл дополнять, но похоже что контекст создается внутри RunMessageLoopInner:
// Register marshaller for background tasks.  At this point,
                    // need to be able to successfully get the handle to the
                    // parking window.  Only do it when we're entering the first
                    // message loop for this thread.
                    if (messageLoopCount == 1) {
                        WindowsFormsSynchronizationContext.InstallIfNeeded();
                    }

Примерно тут Ref source

… а также, неявно, в конструкторе любого контрола.

Sign up to leave a comment.

Articles