В asp.net приложении на .net 4.8 есть синхронный акшн, в глубинах реализации (через длинную цепочку вызовов) надо использовать асинхронный метод (c кивордом async который).
упрощённый пример ситуации ниже
static async Task<bool> DoWork(string msg) {
//some work
await Task.Delay(1000); //так виснет
await Task.Delay(1000).ConfigureAwait(false); //так норм!return true;
}
//метод контроллераpublic void SomeControllerAction() {
var t = DoWork("test");
var r = t.Result; //тут виcнет
//other work
}
дак вот, при вызове у таски, возвращённой от async функции, .Result — на ней виснет и управление не возвращается.
виснет после 1-го вызова await в async функции.
суть проблемы раскопал — оказывается по умолчанию, после await, система пытается возвратить управление в исходный поток из которого был вызван await
но, исходный поток заблокирован на .Result — deadlock
в качестве лечения предлагается — после вызова таски для await добавлять .ConfigureAwait(false)
это типа не принуждает после await-а продолжить выполнение на исходном потоке.
такое поведение вызывает вопросы
1. зачем такое дефолтное поведение в asp.net? ведь при async/await-ах и так подразумевается что после окончания ожидания управление может продолжено свободным потоком из пула.
идея продолжения в исходном хуже и с точки зрения перфа — исходный поток мог уже быть назначен для выполнения другой таски и занят.
понятен кейс такого поведения — в UI приложениях, актуально чтоб продолжили в том же потоке откуда было вызвано, но для этого
в таких случаях можно и вызывать .ConfigureAwait(true) или даже вызывать только на рутовой таске, а вложенные наследуют заданное поведение (await происходит же из таски, и можно получить доступ к её опциям).
либо какую-то глобальную настройку выставлять в UI приложениях.
в asp.net так не надо, т.е. из-за частного случая сделали неудобное поведение.
2. почему нет глобальной настройки? или для рутовой таски указать стратегию для вложенных await-ах?
3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке
это какое-то неудачно решение дизайна?
или есть какие-то причины которые от меня ускользнули?
MH>такое поведение вызывает вопросы MH>1. зачем такое дефолтное поведение в asp.net? ведь при async/await-ах и так подразумевается что после окончания ожидания управление может продолжено свободным потоком из пула. MH>идея продолжения в исходном хуже и с точки зрения перфа — исходный поток мог уже быть назначен для выполнения другой таски и занят.
Потому что ASP.NET и IIS устроен так, что доступ к контексту запроса возможет только из одного потока. Кроме того, если, не дай бог, вы используете WebForms, то там еще и свой ЖЦ есть и асинхронные вызовы должны сихронизироваться.
MH>в asp.net так не надо, т.е. из-за частного случая сделали неудобное поведение.
Не частного. В общем случае архитектура веб-серверов такова, что каждый запрос обрабатывает выделенный поток.
MH>2. почему нет глобальной настройки? или для рутовой таски указать стратегию для вложенных await-ах?
Конечно есть, SynchronizationContext, подробнее по ссылке https://learn.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext
MH>3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке
Во-первых не в каждом, а только в том, для которого вызывается Wait() или Result.
Во-вторых в общем случае не надо делать sync-over-async. Это всегда признак плохого кода. Возможно не вашего, но все равно плохого.
MH>это какое-то неудачно решение дизайна?
Очень удачное
MH>или есть какие-то причины которые от меня ускользнули?
Конечно. Вообще наивно думать, что Microsoft за 10 лет не вылизал до идеала с точки зрения дизайна и быстродействия всю систему с async\await.
Здравствуйте, gandjustas, Вы писали:
MH>>1. зачем такое дефолтное поведение в asp.net? .. G>Потому что ASP.NET и IIS устроен так, что доступ к контексту запроса возможет только из одного потока.
это не так. доступ из разных потоков возможен. проблемы могут быть при одновременном доступе из разных потоков, но это не относится к async/await.
G>Кроме того, если, не дай бог, вы используете WebForms, то там еще и свой ЖЦ есть и асинхронные вызовы должны сихронизироваться.
тут незнаю, но к счастью не мой кейс.
MH>>в asp.net так не надо, т.е. из-за частного случая сделали неудобное поведение. G>Не частного. В общем случае архитектура веб-серверов такова, что каждый запрос обрабатывает выделенный поток.
дак нет же. при асинхронности, продолжить выполнение может другой свободный поток пула и это лучше и с точки зрения перфа.
частный случая типа легаси типа WebForms (если там конечно так) — частный случай.
MH>>2. почему нет глобальной настройки? или для рутовой таски указать стратегию для вложенных await-ах? G>Конечно есть, SynchronizationContext, подробнее по ссылке https://learn.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext
спасибо почитаю, но я проверял и перед вызовом асинк-метода SynchronizationContext пуст ( SynchronizationContext.Current — null).
MH>>3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке G>Во-первых не в каждом, а только в том, для которого вызывается Wait() или Result.
это не так. для таска, для которого вызывается .Result — не срабатывает (только что проверил).
а вот если внутри async есть await и у него вызвано .ConfigureAwait(false) — то внутри уже действительно можно не вызывать .ConfigureAwait(false)
G>Во-вторых в общем случае не надо делать sync-over-async. Это всегда признак плохого кода. Возможно не вашего, но все равно плохого.
понятно что лучше делать всю цепочку async. но к сожалению бывает когда это разным причинам неудастся.
MH>>или есть какие-то причины которые от меня ускользнули? G>Конечно. Вообще наивно думать, что Microsoft за 10 лет не вылизал до идеала с точки зрения дизайна и быстродействия всю систему с async\await.
не сильно ошибусь если скажу что развитие .net framework остановилось лет 5 назад, и некоторые вещи остались как есть.
Здравствуйте, MadHuman, Вы писали:
MH>Здравствуйте, gandjustas, Вы писали:
MH>>>1. зачем такое дефолтное поведение в asp.net? .. G>>Потому что ASP.NET и IIS устроен так, что доступ к контексту запроса возможет только из одного потока. MH>это не так. доступ из разных потоков возможен. проблемы могут быть при одновременном доступе из разных потоков, но это не относится к async/await.
Как же не так?
HttpContext.Current в другом потоке будет null. И это напрямую относится к async\await, так как его основное назначение — писать асинхронный код как синхронный, не накладывая дополнительные сложности на программиста.
Можно было было сделать asp.net так, чтобы он не требовал синхронизации контекста. Возможно даже в core так работает, не проверял. Но в .NET FW — увы, один запрос — один поток.
MH>>>в asp.net так не надо, т.е. из-за частного случая сделали неудобное поведение. G>>Не частного. В общем случае архитектура веб-серверов такова, что каждый запрос обрабатывает выделенный поток. MH>дак нет же. при асинхронности, продолжить выполнение может другой свободный поток пула и это лучше и с точки зрения перфа.
Если у нас две параллельные асинхронные операции, в каком потоке будет продолжаться выполнение?
MH>>>3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке G>>Во-первых не в каждом, а только в том, для которого вызывается Wait() или Result. MH>это не так. для таска, для которого вызывается .Result — не срабатывает (только что проверил). MH>а вот если внутри async есть await и у него вызвано .ConfigureAwait(false) — то внутри уже действительно можно не вызывать .ConfigureAwait(false)
Это я не очень точно сформулировал. ConfigureAwait нужен только на нижнем уровне, где реально происходит ожидание, а не на верхнем.
G>>Во-вторых в общем случае не надо делать sync-over-async. Это всегда признак плохого кода. Возможно не вашего, но все равно плохого. MH>понятно что лучше делать всю цепочку async. но к сожалению бывает когда это разным причинам неудастся.
За много лет я встретил лишь пару таких случаев. В основном это легаси фреймворки, которые не адаптированы под async. Но это как раз редкие исключения, а не общая практика.
Здравствуйте, gandjustas, Вы писали:
MH>>>>1. зачем такое дефолтное поведение в asp.net? .. G>>>Потому что ASP.NET и IIS устроен так, что доступ к контексту запроса возможет только из одного потока. MH>>это не так. доступ из разных потоков возможен. проблемы могут быть при одновременном доступе из разных потоков, но это не относится к async/await. G>Как же не так? G>HttpContext.Current в другом потоке будет null.
это конечно, но хорошая практика — передавать контекст параметром, тогда такой проблемы нет.
у меня нет ни одного такого HttpContext.Current использования.
G>Но в .NET FW — увы, один запрос — один поток.
ну это не то чтоб жесткое ограничение.
то есть в рамках обработки реквеста можно запустить сколь угодно многопоточную работу, при условии что из неё не будет доступа к HttpContext.
если разные потоки обращаются к одному контексту (или его свойствам) последовательно — то проблем нет.
MH>>>>в asp.net так не надо, т.е. из-за частного случая сделали неудобное поведение. G>>>Не частного. В общем случае архитектура веб-серверов такова, что каждый запрос обрабатывает выделенный поток. MH>>дак нет же. при асинхронности, продолжить выполнение может другой свободный поток пула и это лучше и с точки зрения перфа. G>Если у нас две параллельные асинхронные операции, в каком потоке будет продолжаться выполнение?
пример странный (или я его не понял).
но исходя из того как понял — каждая операция продолжится либо на свободном тредпульном потоке либо на том на котором возник await (как раз зависит от ConfigureAwait(false) )
но опять же как связан вопрос, с моим исходным тезисом его вызвавшем — неясно.
MH>>понятно что лучше делать всю цепочку async. но к сожалению бывает когда это разным причинам неудастся. G>За много лет я встретил лишь пару таких случаев. В основном это легаси фреймворки, которые не адаптированы под async. Но это как раз редкие исключения, а не общая практика.
вот у меня как раз легаси случай.
хотя не соглашусь что это экзотика. раз async api теперь основной тренд, то удобный (без лишней когнитивной нагрузки) sync over async — это хорошо.
к тому же особых проблем для этого нет (кроме конечно исходно озвученной проблемы с .Result)
Здравствуйте, MadHuman, Вы писали:
MH>такое поведение вызывает вопросы MH>1. зачем такое дефолтное поведение в asp.net?
Многопоточить в рамках одного реквеста смысла не имеет, только потеря перфоманса, async/await там имеют смысл только на ожидании IO. Поэтому дефолтный контекст синхронизации не порождает потоков.
В ASP.NET Core это поменяли.
MH>2. почему нет глобальной настройки?
Есть. Можно заменить SynchronizationContrext.Current, можно задать свой task scheduler.
MH>или для рутовой таски указать стратегию для вложенных await-ах?
Увы, но нет. CfgAwait нужно делать на каждом await.
Здравствуйте, MadHuman, Вы писали:
G>>HttpContext.Current в другом потоке будет null. MH>это конечно, но хорошая практика — передавать контекст параметром, тогда такой проблемы нет.
Конечно, но 20 лет назад, когда изобретали ASP.NET об этом не сильно думали.
MH>у меня нет ни одного такого HttpContext.Current использования.
А в ASP.NET есть, весьма вероятно даже в коде, вызываемом из вашего кода.
G>>Но в .NET FW — увы, один запрос — один поток. MH>ну это не то чтоб жесткое ограничение.
В каком смысле? ASP.NET в FW сделан так.
Когда поток завершается завершается запрос. В конце концов все продолжения асинхронных операций должны выполниться в потоке запроса.
MH>то есть в рамках обработки реквеста можно запустить сколь угодно многопоточную работу, при условии что из неё не будет доступа к HttpContext.
В этом случае запускать новые потоки смысла не имеет, вы не сможете воспользоваться результатами их работы в рамках вашего запроса или вам придется вручную городить синхронизацию с сериализацию.
MH>если разные потоки обращаются к одному контексту (или его свойствам) последовательно — то проблем нет.
Как вы будете синхронизировать доступ? Как вы будете отслеживать что запрос не завершился?
MH>>>дак нет же. при асинхронности, продолжить выполнение может другой свободный поток пула и это лучше и с точки зрения перфа. G>>Если у нас две параллельные асинхронные операции, в каком потоке будет продолжаться выполнение? MH>пример странный (или я его не понял). MH>но исходя из того как понял — каждая операция продолжится либо на свободном тредпульном потоке либо на том на котором возник await (как раз зависит от ConfigureAwait(false) )
Верно. Теперь представим что у вас две операции: await Task.WaitAny(task1, task2) в каком потоке должно запуститься продолжение?
Здравствуйте, gandjustas, Вы писали:
MH>>у меня нет ни одного такого HttpContext.Current использования. G>А в ASP.NET есть, весьма вероятно даже в коде, вызываемом из вашего кода.
не факт. у меня есть код который работает таким образом — из многих потоков обращается к HttpContext и вглубь его (с синхронизацией естественно) и всё много лет работает стабильно.
но конечно утверждать что такого кода нет — я не могу, но с высокой долей можно предположить что за эти 20лет от него уже должны избавиться.
MH>>то есть в рамках обработки реквеста можно запустить сколь угодно многопоточную работу, при условии что из неё не будет доступа к HttpContext. G>В этом случае запускать новые потоки смысла не имеет, вы не сможете воспользоваться результатами их работы в рамках вашего запроса или вам придется вручную городить синхронизацию с сериализацию.
это зависит от задачи. и да, конечно придется делать синхронизацию.
MH>>>>дак нет же. при асинхронности, продолжить выполнение может другой свободный поток пула и это лучше и с точки зрения перфа. G>>>Если у нас две параллельные асинхронные операции, в каком потоке будет продолжаться выполнение? MH>>пример странный (или я его не понял). MH>>но исходя из того как понял — каждая операция продолжится либо на свободном тредпульном потоке либо на том на котором возник await (как раз зависит от ConfigureAwait(false) ) G>Верно. Теперь представим что у вас две операции: await Task.WaitAny(task1, task2) в каком потоке должно запуститься продолжение?
хороший пример.
продолжение групповой таски должно запуститься на том же потоке из которого вызван await (в силу текущей логики работы asp.net синхронизации).
а вот task1 task2 — по хорошему бы запустить паралельно на разных потоках, но опять же в силу текущей логики синхронизации — они будут выполняться последовательно, что конечно плохо
с точки зрения скалируемости, и если это cpu-bound таски без авайтов, то такая конструкция полностью лишается смысла (всегда будет выполняться 1й таск, до 2-го даже не дойдёт).
MH>>>>3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке G>>>Во-первых не в каждом, а только в том, для которого вызывается Wait() или Result. MH>>это не так. для таска, для которого вызывается .Result — не срабатывает (только что проверил). MH>>а вот если внутри async есть await и у него вызвано .ConfigureAwait(false) — то внутри уже действительно можно не вызывать .ConfigureAwait(false) G>Это я не очень точно сформулировал. ConfigureAwait нужен только на нижнем уровне, где реально происходит ожидание, а не на верхнем.
Такая ситуация может быть на нескольких уровнях. Т.е. везде нужен .ConfigureAwait(false).
Но что-делать, если прооблемный код в проекте, который нельзя поменять на данный момент? Или в third-party библиотеке?
Можно попробовать поменять SynchronizationContext.Current, как предлагают здесь:
Task t;
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // will still target the original context
MH>>>>1. зачем такое дефолтное поведение в asp.net? .. G>>>Потому что ASP.NET и IIS устроен так, что доступ к контексту запроса возможет только из одного потока. MH>>это не так. доступ из разных потоков возможен. проблемы могут быть при одновременном доступе из разных потоков, но это не относится к async/await. G>Как же не так? G>HttpContext.Current в другом потоке будет null. И это напрямую относится к async\await, так как его основное назначение — писать асинхронный код как синхронный, не накладывая дополнительные сложности на программиста. G>Можно было было сделать asp.net так, чтобы он не требовал синхронизации контекста. Возможно даже в core так работает, не проверял. Но в .NET FW — увы, один запрос — один поток.
What has changed, however, is whether certain environments publish their own SynchronizationContext.
In particular, whereas the classic ASP.NET on .NET Framework has its own SynchronizationContext, in contrast ASP.NET Core does not.
That means that code running in an ASP.NET Core app by default won’t see a custom SynchronizationContext, which lessens the need for ConfigureAwait(false) running in such an environment.
оказалось ошибочно что я посчитал SynchronizationContext пустым (хз как так вышло, вроде перепроверял), он есть и это — AspNetSynchronizationContext.
это (то что он пуст в sync action-е контроллера под mvc4) и вызывало непонятки.
теперь как говорится, всё встало на свои места.
Здравствуйте, MadHuman, Вы писали:
MH>С Новым Годом!)
MH>такое поведение вызывает вопросы MH>2. почему нет глобальной настройки? или для рутовой таски указать стратегию для вложенных await-ах? MH>3. зачем об этом думать при каждом await ? а .ConfigureAwait(false) надо вызвать при каждом во всей цепочке
Пока наиболее активны (если я ничего не пропустил) пара направлений для доработки:
MH>это какое-то неудачно решение дизайна? MH>или есть какие-то причины которые от меня ускользнули?
На мой взгляд, это, в целом, удачное решение, но недоработанное. Мне пока не сложно добавлять `.ConfigureAwait(…)`, но, надеюсь, лучшие умы смогут найти хорошее решение для этой рутины.
Help will always be given at Hogwarts to those who ask for it.