Почему SwitchTo был удален из Async CTP / Release?

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

Затем я нашел это объявление здесь:

Причина, по которой мы избавились от него, заключалась в том, что это было очень опасно. Альтернатива - связать свой код внутри TaskEx.Run ...

У меня простой вопрос: Почему это было опасно? К каким конкретным опасностям может привести его использование?

Обратите внимание, что я прочитал оставшуюся часть этого сообщения, поэтому я понимаю, что здесь есть технические ограничения. У меня все еще вопрос: если я знаю об этом, почему это опасно?

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

В частности, очень наивно, вот как я бы рассмотрел реализацию требуемых методов:

public static class ContextSwitcher
{
    public static ThreadPoolContextSwitcher SwitchToThreadPool()
    {
        return new ThreadPoolContextSwitcher();
    }

    public static SynchronizationContextSwitcher SwitchTo(this SynchronizationContext synchronizationContext)
    {
        return new SynchronizationContextSwitcher(synchronizationContext);
    }
}

public class SynchronizationContextSwitcher : INotifyCompletion
{
    private readonly SynchronizationContext _SynchronizationContext;

    public SynchronizationContextSwitcher(SynchronizationContext synchronizationContext)
    {
        _SynchronizationContext = synchronizationContext;
    }

    public SynchronizationContextSwitcher GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public void OnCompleted(Action action)
    {
        _SynchronizationContext.Post(_ => action(), null);
    }

    public void GetResult()
    {
    }
}

public class ThreadPoolContextSwitcher : INotifyCompletion
{
    public ThreadPoolContextSwitcher GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public void OnCompleted(Action action)
    {
        ThreadPool.QueueUserWorkItem(_ => action(), null);
    }

    public void GetResult()
    {
    }
}

Это позволило бы мне написать такой код:

public async void Test()
{
    await ContextSwitcher.SwitchToThreadPool(); // ensure we're not bogging down the UI thread
    // do some heavy processing
    await _UIContext.SwitchTo(); // presumably saved from the main thread
    // update UI with new data
}

person Lasse V. Karlsen    schedule 12.03.2013    source источник
comment
Ха. Это довольно старая ветка! Я никогда не был поклонником Microsoft, иногда это по вашему собственному разумному мнению.   -  person Cory Nelson    schedule 12.03.2013
comment
С тех пор я переключился на await Task.Run(async ()=>{}) - не для того, чтобы избежать пустых опасностей, а просто потому, что мне кажется, что это легче читать. Тем не менее, я считаю, что ваше представление о том, как реализовать SwitchTo(), здраво.   -  person Cory Nelson    schedule 12.03.2013
comment
Не знал о синтаксисе async ()=>{}, требует дальнейшего изучения, спасибо!   -  person Lasse V. Karlsen    schedule 12.03.2013
comment
Я бы посоветовал использовать Send(), а не Post(), потому что, согласно этому сообщению в блоге, это будет означает, что для DispatcherSynchronizationContext именно потоки, не относящиеся к пользовательскому интерфейсу, переключатся на поток пользовательского интерфейса, а поток пользовательского интерфейса сохранит контекст. С Post(), ожидание будет обходить диспетчер, испортить трассировку стека, даже если вы ожидаете в потоке пользовательского интерфейса. И для всех остальных SynchronizationContexts это все равно не имело бы значения. (Тогда они не будут меняться, что, я думаю, тоже нормально).   -  person John    schedule 25.06.2017


Ответы (3)


Стивен Тауб (Stephen Toub) имеет дополнительную информацию о рассуждениях в эта ветка.

Подводя итог, это не лучшая идея по двум причинам:

  1. Он продвигает неструктурированный код. Если вам нужно выполнить «тяжелую обработку», ее следует поместить в Task.Run. Еще лучше отделите бизнес-логику от логики пользовательского интерфейса.
  2. Обработка ошибок и (некоторые) продолжения выполняются в неизвестном контексте. _2 _ / _ 3_ блоки в Test должны будут обрабатывать выполнение в пуле потоков или контексте пользовательского интерфейса (и если они работают в контексте пула потоков, они не могут использовать SwitchTo для перехода в контекст пользовательского интерфейса ). Кроме того, пока вы await возвращаемый Task, все будет в порядке (await при необходимости исправит контекст продолжения), но если у вас есть явные ContinueWith продолжения, использующие ExecuteSynchronously, то у них будет та же проблема, что и у блоков _11 _ / _ 12_ .

Короче говоря, без SwitchTo код чище и предсказуемее.

person Stephen Cleary    schedule 12.03.2013
comment
Хорошо, это проливает на это больше света. По-прежнему выглядит странно, что этот пост создает всевозможные проблемы с неизвестными контекстами, а затем переходит к демонстрации того, как реализовать поддержку в любом случае. - person Lasse V. Karlsen; 12.03.2013
comment
@ LasseV.Karlsen Я думаю, что такое отношение не редкость для разработчиков .Net. Насколько я понимаю, они говорят что-то вроде: «Если бы мы предоставили эту функцию, люди подумали бы, что ее можно использовать, поэтому они бы использовали ее часто, а это плохо. Но если он размещен в каком-то блоге, они, вероятно, будут использовать его только тогда, когда им это действительно нужно, и они поймут, что на самом деле происходит, и это нормально ». - person svick; 13.03.2013
comment
@BrunoMartinez: Это совсем другое. SwitchTo может вызвать серьезные проблемы с неоднозначным контекстом; количество аргументов, переданных методу, не вызывает подобных проблем. Кроме того, SwitchTo просто не предоставлял опасный API; удаляя его из API, они снижают сложность того, что они должны предоставлять. Ограничение количества аргументов метода - это добавление функциональности и фактически создаст целая куча работы. - person Stephen Cleary; 07.02.2014
comment
@StephenCleary Я думаю, вы хотели ответить на мой ответ ниже. - person Bruno Martinez; 20.08.2014

ConfigureAwait на самом деле более опасен, чем SwitchTo. Мысленно отследить текущий контекст и последний вызов SwitchTo не сложнее, чем отследить, где в последний раз была назначена переменная. С другой стороны, ConfigureAwait переключает контекст тогда и только тогда, когда вызов действительно выполняется асинхронно. Если задача уже выполнена, контекст сохраняется. Вы не можете это контролировать.

person Bruno Martinez    schedule 06.02.2014

По словам Дэвида Фаулера и Стивена Туба в проблема с GitHub, поскольку для await внутри _3 _ / _ 4_ больше нет ограничений.

ИМО, явно лучше использовать что-то вроде await TaskScheduler.Default.SwitchTo(), чем полагаться на ConfigureAwait(false) в коде сторонней библиотеки, особенно если мы хотим убедиться, что код не выполняется в каком-либо настраиваемом контексте синхронизации. У меня есть сообщение в блоге с более подробной информацией об этом, включая экспериментальную реализацию SwitchTo.

Вкратце, я считаю, что первый вариант ниже четко указывает на намерение с минимальным стандартным кодом:

// switch to the thread pool explicitly for the rest of the async method
await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
// execute RunOneWorkflowAsync on the thread pool 
// and stay there for the rest of the async method
await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false);
await RunAnotherWorkflowAsync();
await Task.Run(async () => 
{
  // start on the thread pool
  await RunOneWorkflowAsync();
  await RunAnotherWorkflowAsync();
}).ConfigureAwait(false);
// continue on the thread pool for the rest of the async method
// start on whatever the current synchronization context is
await RunOneWorkflowAsync().ConfigureAwait(false);
// continue on the thread pool for the rest of the async method,
// unless everything inside `RunOneWorkflowAsync` has completed synchronously
await RunAnotherWorkflowAsync();
person noseratio    schedule 28.09.2020
comment
Я не уверен, что await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false); гарантирует, что остальная часть асинхронного метода будет работать в пуле потоков. Разве невозможно переключение потока (навязанное операционной системой) сразу после создания задачи и непосредственно перед ее ожиданием, чтобы при возобновлении потока обнаружилось, что задача уже завершена? В этом случае ConfigureAwait(false) не будет иметь никакого эффекта, потому что он влияет только на ожидание незавершенных задач. Например, await Task.CompletedTask.ConfigureAwait(false); не действует. - person Theodor Zoulias; 28.09.2020
comment
@TheodorZoulias, это определенно может стать проблемой для await RunOneWorkflowAsync().ConfigureAwait(false). Если все внутри RunOneWorkflowAsync завершается синхронно, мы продолжим синхронно в исходном контексте. - person noseratio; 28.09.2020
comment
Что касается await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false), моя интуиция подсказывает мне, что это всегда будет продолжаться ThreadPool, но я должен это проверить, я вернусь с результатами :) - person noseratio; 28.09.2020
comment
Я не пытался доказать, что это возможно, и может быть невозможно по какой-то внутренней причине. Я бы попытался проверить это, запустив сотни потоков, чтобы операционная система часто переключалась с потока на поток. - person Theodor Zoulias; 28.09.2020
comment
@TheodorZoulias, поэтому с Task.Run(RunOneWorkflowAsync) ожидается, что вызов RunOneWorkflowAsync будет запланирован для пула потоков ( вот как, TaskCreationOptions.LongRunning не применяется). Теоретически это может оказаться тот же поток, который вызвал Task.Run (скажем, если вызывающий метод завершился ненадолго и этот поток был возвращен в пул до, наша задача будет удалена из очереди). Тем не менее, это все равно будет асинхронное продолжение в очереди, то есть не произойдет того же кадра стека. - person noseratio; 28.09.2020
comment
... в этом случае, даже если в этом потоке пула был контекст синхронизации, ConfigureAwait(false) переставит продолжение в другой поток пула. Думать обо всех этих угловых случаях сложно, это еще одна причина, почему мне нравится простота await TaskScheduler.Default.SwitchTo() :) - person noseratio; 28.09.2020
comment
Да, я бы не был уверен в переключателе без ожидающего с жестко запрограммированным false в качестве возвращаемого значения его _ 2_. :-) - person Theodor Zoulias; 28.09.2020
comment
@TheodorZoulias это именно то, что у меня есть alwaysSchedule param :) com / noseratio / - person noseratio; 28.09.2020
comment
Есть ли новости по этому поводу от вас, ребята, которые находятся в самой гуще событий? Будет ли одно ожидание Task.Run (() = ›{int test = 1;}). ConfigureAwait (false); эффективно добиться того же, что и SwitchTo? Затем вы можете использовать Dispatcher.InvokeAsync, если вам нужно вернуться к потоку пользовательского интерфейса. Я что-то неправильно понял? - person rolls; 20.06.2021
comment
@rolls, не совсем так. await TaskScheduler.Default.SwitchTo() может не работать, если это уже поток пула без какого-либо контекста синхронизации. Task.Run обычно ставит задачу в очередь (проще говоря, думайте ThreadPool.QueueUserWorkItem), плюс другие незначительные накладные расходы на распределение задачи и т. Д. То есть, правильная реализация SwitchTo была бы немного более эффективной. - person noseratio; 20.06.2021
comment
Кроме того, мне лично не нравится идея обратного переключения и принудительного переключения между пулом потоков и потоком пользовательского интерфейса в рамках одного и того же метода. Я бы предпочел использовать await TaskScheduler.Default.SwitchTo() как инструмент, чтобы убедиться, что остальная часть моего async метода выполняется в пуле потоков. - person noseratio; 20.06.2021
comment
Если у вас есть iprogress, который должен выполняться в потоке пользовательского интерфейса, и очень медленный метод, который вы запускаете в фоновом потоке, как вы это делаете без постоянного переключения контекста? Это моя проблема. - person rolls; 20.06.2021
comment
@rolls Вам не нужен Dispatcher.InvokeAsync для шаблона IProgress, но да, я бы сделал исключение для уведомлений о ходе выполнения;) Имейте в виду, что progress.Report(i) не является ожидаемым, но он все еще асинхронный, он использует SynchronizationContext.Post. У меня есть старый связанный ответ: https://stackoverflow.com/questions/21611292/cancel-thread-and-restart-it/21639075#21639075 - person noseratio; 20.06.2021