Pull to refresh

Comments 46

Вы перевели англоязычную статью написанную вами же?
Я правильно понял? :)
За это должна выдаваться специальная ачивка :)
Продолжение цикла «вредные советы» на хабре.

Если вы разрабатываете стороннюю библиотеку, очень важно всегда настраивать await таким образом, чтобы остальная часть метода была выполнена произвольным потоком из пула.
Вот только это заставит вас писать тот самый некрасивый код, который вы так хотели избежать. Браво! А проблема всего лишь в том, что не надо вызывать get_Result вручную, блокируя поток.

Когда какой-то библиотеке надо вернуться в оригинальный контекст, то на это скорее всего есть веская причина — например, библиотека хочет вызвать колбек. Если же такой причины нет, то запрещать ей возвращаться в оригинальный поток надо не средствами ConfigureAwait, а через Task.Run.

… использовать асинхронную версию метода Read:
Вот только конструктор FileStream блокирующий, и вы блокируете UI поток. Метод, который вы отмели как «некорректный», гораздо корректнее вашего.
Если же такой причины нет, то запрещать ей возвращаться в оригинальный поток надо не средствами ConfigureAwait, а через Task.Run.
Из вашего объяснения я не понял, почему следует использовать Task.Run вместо ConfigureAwait. Не могли бы вы пояснить этот момент более подробно?
Вот только это заставит вас писать тот самый некрасивый код, который вы так хотели избежать. Браво!

Вы не правы. Запустите этот код и посмотрите чему равна переменная areEqual:

private async void btnRead_Click(object sender, EventArgs e)
{
    Context currentContext1 = Thread.CurrentContext;
    int result = await DoSomeWorkAsync();
    Context currentContext2 = Thread.CurrentContext;
    bool areEqual = ReferenceEquals(currentContext1, currentContext2);
}

private async Task<int> DoSomeWorkAsync()
{
    await Task.Delay(100).ConfigureAwait(false);
    return 1;
}

ConfigureAwait(false) в методах, вызываемых вшешним методом не приводят к продолжению выполнения внешнего метода (в этом примере — btnRead_Click) в произвольном потоке, это указывает, как выполнять только внутренний метод (DoSomeWorkAsync). Это происходит потому, что каждый метод, помеченный async, разворачивается в state machine независимо от тех, которые он вызывает.
Вы ранее писали:
… в коде сторонних библиотек всегда необходимо добавлять ConfigureAwait(false).
Отсюда я делаю вывод, что DoSomeWorkAsync — это код библиотеки. Если ей нужно зачем-то переключать контекст, то это происходит, скорее всего, для вызова колбека, в котором вам придется использовать ручное переключение контекста, если библиотека делает такую гадость, как ConfigureAwait(false). Если же библиотеке не нужно переключать контекст, то её код не является асинхронным, и помечать его асинхронным не требуется, а для последующего исполнения в другом потоке можно и нужно использовать, например, Task.Run.
ConfigureAwait(false) во внутреннем методе не оказывает влияния на контекст выполнения во внешнем методе.

private async Task F1()
{
    // 1. Контекст тот же, что у вызывающего метода
    await F2();
    // 4. Контекст тот же, что у вызывающего метода, 
    // ConfigureAwait(false) в F1 не повлиял на смену контекста в этом методе
}

private async Task F2()
{
    // 2. Контекст тот же, что у вызывающего метода
    await Task.Delay(100).ConfigureAwait(false);
    // 3. Нет контекста. Поток, выполняющий эту часть метода - произвольный из пула
}


Проставив ConfigureAwait(false), автор библиотеки не сделает никакой гадости, он таким образом повлияет только на контекст выполнения оставшейся части метода (пункт 3).
Использование Task.Run приводит к неоптимальной утилизации потоков, что для сторонней библиотеки — серьезное дело, если автор рассчитывает на то, что она будет использована в высоконагруженных проектах
Я вам про одно, вы мне про другое. Третий раз не буду объяснять.
А проблема всего лишь в том, что не надо вызывать get_Result вручную, блокируя поток.

Как я написал в статье, не всегда возможно использовать асинхронные методы. К примеру child actions в ASP.NET MVC их не поддерживают. Также, если вы работаете с legacy ASP.NET WebForms, то там их использовать также будет проблематично. Все это приводит к тому, что библиотеки необходимо писать с учетом того, что клиентский код будет обращаться к Result проперти, а не работать через await

Когда какой-то библиотеке надо вернуться в оригинальный контекст, то на это скорее всего есть веская причина — например, библиотека хочет вызвать колбек. Если же такой причины нет, то запрещать ей возвращаться в оригинальный поток надо не средствами ConfigureAwait, а через Task.Run.

Внутренний метод не может определять каким потоком будет выполнена оставшаяся часть внешнего метода (см. комментарий выше)
Когда вам требуется вызвать асинхронный код из синхронного, то это ваша обязанность разруливать переключения контекста, а не самого асинхронного кода. Например, вы можете его вызвать в отдельном потоке, а затем дождаться его. Например, через Task.Run, хотя я не уверен, что это сработает в ASP.NET.
В ASP.NET лучше с Task.Run не баловаться, так как потоки жрутся из тред пула который лучше расходовать на обработку запросов, ну при условии что у вас стандартный TaskScheduler.
В данном случае это не так важно, поток будет взят в любом случае.
>> Например, вы можете его вызвать в отдельном потоке, а затем дождаться его.

И что, вы думаете, это как-то поможет с описанным в статье дедлоком?
Разумеется.
void button1_Click(object sender, EventArgs e)
{
    object result = Task.Run(async () => await DoSomeWorkAsync()).Result;
}

async Task<object> DoSomeWorkAsync()
{
    await Task.Delay(100);
    return null;
}
Отлично, т.е. у вас вызывающий код должен быть в курсе деталей реализации DoSomeWorkAsync, чтобы знать, нужно его оборачивать в Task.Run или нет.

Или вы всерьез предлагаете вообще все таски так оборачивать «на всякий случай»?

Не лучше ли делать так, как рекомендуют авторы всего этого дела, и по возможности писать контексто-независимый асинхронный код (т.е. — с ConfigureAwait), особенно в библиотеках, где его планируется повторно использовать?
Вы не внимательно читали топик. Это workaround на случай вызова асинхронного кода из синхронного.
Если сразу писать код с ConfigureAwait, то ваш воркараунд не нужен. Причем профит этим не ограничивается — не будет ненужных переходов на главный поток, вне зависимости от контекста вызова, и не будет разницы в поведении из-за неизвестного начального шедулера.
Просто оборачивая синхронный код в Task.Run(), а ещё хуже в Task.Factory.StartNew(), в ASP.NET мы не получим ожидаемых результатов. Эти методы только выглядят асинхронно. Такой код всё равно будет блокировать рабочий процесс в пуле на время выполнения задачи. При этом, мы получаем проблемы с SynchronizationContext. Правильный подход заключается в вызове асинхронных методов и использовании async/await.

Task.Run() полезно использовать для выполнения CPU-нагруженных задач в асинхронном режиме для UI-приложений, но не в ASP.NET.
В .NET 4.5.2 появился новый метод QueueBackgroundWorkItem, который позволяет надежно планировать и выполнять фоновые процессы в ASP.NET в случае необходимости.
Судя по минусам, у аудиотории существуют сомнения в моей правоте. Поэтому вот падающий код с подходом автора топика:

async void button1_Click(object sender, EventArgs e)
{
    await DoSomeWorkAsync(ReportProgress);
}

void ReportProgress()
{
    if (this.InvokeRequired)
        throw new Exception();
}

async Task DoSomeWorkAsync(Action progressReporter)
{
    progressReporter();
    await Task.Delay(100).ConfigureAwait(false);
    progressReporter();
}

Исправляется, как я и сказал, тем самым некрасивым кодом из до-async эры.
А всего-то нужно не изобретать чёрти что, а использовать рекомендуемый паттерн с IProgress(Of T):

async void button1_Click(object sender, EventArgs e)
{
    var progressReporter = new Progress<object>(ReportProgress);
    await DoSomeWorkAsync(progressReporter);
}

void ReportProgress(object value)
{
    if (this.InvokeRequired)
        throw new Exception();
}

async Task DoSomeWorkAsync(IProgress<object> progressReporter)
{
    progressReporter.Report(null);
    await Task.Delay(100).ConfigureAwait(false);
    progressReporter.Report(null);
}

Progress(Of T) позаботится о том, чтобы захватить требуемый контекст.
И сразу же наткнуться на то, что он вешает UI поток при частых вызовах, потому что не ждет окончания исполнения. А поскольку вы передаёте его библиотеке, вам придется позаботиться об этом, используя ещё менее красивый код, чем был раньше.

Наступал я на такие грабли, да.
вешает UI поток при частых вызовах
Ограничьте количество репортов или не пихайте в обработчик репортов тяжеловесный код, способный повесить поток?
И всё ради того, чтобы добиться возможности вызывать get_Result. Вместо того, чтобы решить эту проблему на стороне клиента, где она и должна решаться, например, через Task.Run (30 символов).
Проблема в самом вызове get_Result. Если вы мешаете асинхронный и синхронный код, то вы ССЗБ и должны быть готовы к появлению проблем. К повсеместному использованию .ConfigureAwait(false), как мне кажется, это отношения не имеет.
Автору топика это скажите :)
Мне хочется надеяться, автор и так понимает проблемы смешивания sync/async. Лично я бы прикопался к формулировке:
Конечно, мы можем использовать await вместо обращения к свойству Result для того, чтобы избежать дедлока
— и написал бы, что нам нужно использовать await вместо get_Result. За исключением случаев, где это невозможно, но, мол, тогда смотрите сами.
Само собой, там где это возможно, следует использовать await. Проблема в том, что это не везде возможно и вы, как автор библиотеки, не можете знать где она будет использоваться.
Плюс при проектировании библиотек всегда следует придерживаться принципа наименьшего удивления, это означает, что ваш код не должен требовать workaround-ов (таких как вызов Task.Run) для корректного выполнения.
Главное с этим async/await не переборщить. Вот под Windows Store библиотека Azure вся асинхронная и насколько же сложнее с ней работать. Я уже все маты сложил, так приемлемой работы и не добился.
Да там все WinRT API по большей части асинхронное без синхронных вариантов. Особенно пикантно это выглядит, когда система вызывает ивенты в UI потоке, а в обработчике тебе нужно дергать асинхронные методы АПИ, что характерно тоже работающие в UI потоке, потому что все запихано в DependencyObject.
Как пример вот мой подводный камень айсберг:
social.msdn.microsoft.com/Forums/ru-RU/5e97ef50-884a-4e79-8432-01ce31e245c0/how-use-paginate-event-async-problem?forum=winappswithcsharp
А в примере с btnRead_Click не будет проблем из-за обращения из рабочего потока к кнопке (btnRead.Enabled = false)?
Нет, потому что произойдет переключение на оригинальный UI поток через его контекст синхронизации. Но следуя советам автора статьи вы вполне на эти проблемы можете нарваться.
Ну хватит уже :) Я в двух комментариях попытался расписать, что проблем не будет до тех пор, пока сам метод btnRead_Click не проставит ConfigureAwait(false). Любые другие методы, которые он вызывает могут проставлять ConfigureAwait(false), это не повлияет на то, что доступ к UI элементам в btnRead_Click произойдет из UI потока.
Это вам хватит людей в заблуждение вводить. Я вам вот здесь и вот здесь описал, почему ваш подход — неверный, и в каких случаях ваш код упадёт.
В примере с btnRead_Click весь обращения к UI элементам происходят из UI потока. В этом как раз самое большое преимущество фичи async/await
Давно интересует такой вопрос, часто вижу код вроде этого:
public async Task<SomeResult> FooAsync()
{
    return await BarAsync();
}


Есть ли какие-то веские причины не переписать его просто так:
public Task<SomeResult> FooAsync()
{
    return BarAsync();
}

т.е. сразу вернуть задачу из метода, без вызова await.

Просто нигде внятного объяснения не могу найти, может кто-то сможет объяснить?
В данном случае никакого, за исключением того, что если вы бросите исключение перед return, то в первом случае оно будет выкинуто при вызове get_Result либо при await, а во втором — сразу.
Разница есть если вы хотите, чтобы после await выполнился еще какой-либо код, если нет — то второй метод даже предпочтительней.
разница будет в StackTrace, если произойдёт исключение.
И если вариант с
return SometingAsync();
используется часто, то понять что-же именно произошло становится сложнее.
Поэтому я предпочитаю пользоваться первым вариантом, если не разрабатываю библиотеку.
если вы бросите исключение перед return, то в первом случае оно будет выкинуто при вызове get_Result либо при await, а во втором — сразу.

Исключение в любом случае будет выброшено сразу. Потому как тело метода всегда выполняется синхронно до первого await.
Нет, разницы никакой нет, кроме сэкономленной копеечке на производительности во втором случае. Если есть возможность возвращать таск и не помечать метод async, то пользоваться вторым вариантом.
Когда то давно написал тест для проверки, в чем конкретно преимущество async подхода в ASP.NET.
Главные вопросы
1. Снимается ли ограничение на 50 потоков на ядро? — Да снимается, потоков заметно больше.
2. Есть ли выигрыш по производительности? — и да и нет. Выигрыш появляется если Вы уперлись в ограничение 50 потоков на ядро, и у Вас есть тяжелые запросы в sql. Тогда да пока sql запросы обрабатываются на sql сервере освободившиеся потоки могут обрабатывать другие запросы.
Дак в этом и смысл async в ASP.NET — неблокирующий I/O.
Посмотрел ваш проект. Видимо очень давно писали тест для проверки. С такой асинхронной реализацией вы не получите никакого преимущества от async/await подхода в ASP.NET:

await Task.Factory.StartNew(() => Thread.Sleep(100));

Это очень плохой код. Вместо Thread.Sleep() надо использовать await Task.Delay(), а вместо Task.Factory.StartNew() новый метод HostingEnvironment.QueueBackgroundWorkItem(). Результаты тестов будут другими.
Спасибо, Да, уже не помню когда — но как только анонсировали асинки. На гитхаб залил гораздо позже.
кстати я тогда обнаружил особенность, что:
Task.WhenAll — стартует все таски сразу.
Task.WhenAny — стартует таски по очереди с интервалом примерно 100мс.

Ну и тестов было много, просто они переписывались поверх. после проверки результатов.
Sign up to leave a comment.

Articles