Всем привет.
Сталкивался ли кто-нибудь с проблемой в стандартном asp.net приложении, когда внутри асинхронного метода контроллера при асинхронном вычитывании тела запроса поток внезапно прерывается без исключения.
Т.е. есть что-то типа
....
public class MyController : ControllerBase {
....
[HttpPost]
[Route("somepost")]
public async Task<string> PostRequest() {
HttpContextLocal context = new HttpContextLocal(HttpContext);
context.SetBody();
log.Info($"Body length after read: {context.body?.Length}");
return await Handler.Application_POST(context);
}
....
}
....
public class HttpContextLocal{
.....
public async void SetBody() {
try {
log.Info($"Body => Position: {context.Request.Body.Position}, CanRead: {context.Request.Body.CanRead}");
var bodyReader = context.Request.BodyReader;
ReadResult readResult;
int cnt=0;
while (true) {
readResult = await bodyReader.ReadAsync();
log.Info($"Read step {++cnt} => IsCompleted: {readResult.IsCompleted}, IsCanceled: {readResult.IsCanceled}");
if (readResult.IsCompleted || readResult.IsCanceled)
break;
log.Info($"Buffer length: {readResult.Buffer.Length}");
bodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
}
body = Encoding.UTF8.GetString(readResult.Buffer.ToArray());
log.Info($"Body read ok => length: {body?.Length}");
....
} catch (Exception ex) {
log.Error(ex);
......
}
}
}
В некоторых ситуациях, прибольшом теле запроса и интенсивной нагрузке на хост вижу в логе примерно вот такую картину...
в логе нет и переменная context.body в методе контроллера пустая, исключния так же не было — такое впечатление, что внутри цикла метода SetBody просто поток прекратил существование
так проставь в самом конролере try catch для того, чтобы наверняка отловить исключение, тогда и видно будет, в чём проблема,
кроме того, не видно, что происходит в конструкторе, может, проблема там?
ещё ты мог бы возвращать не void a Task в SetBody и в контролере ожидать с await
Здравствуйте, -Cheese-, Вы писали:
C>Всем привет. C>Сталкивался ли кто-нибудь с проблемой в стандартном asp.net приложении, когда внутри асинхронного метода контроллера при асинхронном вычитывании тела запроса поток внезапно прерывается без исключения.
C>Т.е. есть что-то типа C>[cs] C>.... C>public class MyController : ControllerBase { C>.... C> [HttpPost] C> [Route("somepost")] C> public async Task<string> PostRequest() { C> HttpContextLocal context = new HttpContextLocal(HttpContext); C> context.SetBody(); C> log.Info($"Body length after read: {context.body?.Length}"); C> return await Handler.Application_POST(context); C> }
C>.... C>} C>....
a await context.SetBody(); не надо поставить?
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, takTak, Вы писали:
T>так проставь в самом конролере try catch для того, чтобы наверняка отловить исключение, тогда и видно будет, в чём проблема, T>кроме того, не видно, что происходит в конструкторе, может, проблема там?
ну оно ж не вывалилось....
T>ещё ты мог бы возвращать не void a Task в SetBody и в контролере ожидать с await
спасибо, это действительно можно попробовать
Здравствуйте, -Cheese-, Вы писали:
C>Сталкивался ли кто-нибудь с проблемой в стандартном asp.net приложении, когда внутри асинхронного метода контроллера при асинхронном вычитывании тела запроса поток внезапно прерывается без исключения.
Есть не перехватываемые исключения вроде StackOverflowException и некоторые виды AV при повреждении памяти. Ты как понимаешь, что исключений не было? Под отладчиком пускал?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
C> context.SetBody();
C> log.Info($"Body length after read: {context.body?.Length}");
C>
Собственно твой код вызывается в том же потоке, что и PostRequest. Скорее всего его тупо рубят на физическом уровне. Твой метод SetBody — это обычный синхронный метод. Модификатор async не делает метод автоматически асинхронным. Скорее всего у тебя об этом ворнинги в выхлоп компилятора пишут, а ты их игнорируешь. Тебе нужно внутри цикла или перед ним вызывать нечто с await (что создаст и запустит новый поток) или самому явно позвать await Task.Run(...).
Кроме того я бы посоветовал после каждого await-выражения дописывать .ConfigureAwait(false), чтобы случайно не захватывать контекст из которого был вызов (а осуществлять продолжение всегда на свободном потоке из пула).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
public async Task<string> PostRequest() {
.....
await context.SetBody();
.....
public class HttpContextLocal{
.....
public async voidTask SetBody() {
.....
Думаю тут так надо. ConfigureAwait(false) вроде по умолчанию для asp.net. можно экспериментально проверить.
Здравствуйте, -Cheese-, Вы писали:
C>Всем привет. C>Сталкивался ли кто-нибудь с проблемой в стандартном asp.net приложении, когда внутри асинхронного метода контроллера при асинхронном вычитывании тела запроса поток внезапно прерывается без исключения.
C>Т.е. есть что-то типа C>
C>....
C>public class MyController : ControllerBase {
C>....
C> [HttpPost]
C> [Route("somepost")]
C> public async Task<string> PostRequest() {
C> HttpContextLocal context = new HttpContextLocal(HttpContext);
C> context.SetBody();
C> }
C>....
C>}
C>....
C>public class HttpContextLocal{
C> public async void SetBody() {}
C>
Попробуй добавить await перед context.SetBody() и возвращаемый тип Task вместо void для SetBody()
Здравствуйте, -Cheese-, Вы писали:
C>в логе нет и переменная context.body в методе контроллера пустая, исключния так же не было — такое впечатление, что внутри цикла метода SetBody просто поток прекратил существование
Здравствуйте, -Cheese-, Вы писали:
T>>ещё ты мог бы возвращать не void a Task в SetBody и в контролере ожидать с await C>спасибо, это действительно можно попробовать
Не "можно", а "нужно". async void — как раз та самая сигнатура, что не сохраняет контекста исключения.
Здравствуйте, VladD2, Вы писали:
VD>Собственно твой код вызывается в том же потоке, что и PostRequest.
Это почему? Из-за void (async void)?
VD>Скорее всего его тупо рубят на физическом уровне.
Что значит "рубят на физическом уровне"?
VD> Тебе нужно внутри цикла или перед ним вызывать нечто с await (что создаст и запустит новый поток) или самому явно позвать await Task.Run(...).
Так есть же в цикле readResult = await bodyReader.ReadAsync();
Здравствуйте, Sharov, Вы писали:
S>Это почему? Из-за void (async void)?
Бай дизайн. Ключевое свойство async ни хрена не приводит к асинхронности (переводу выполнения на другой поток). Запуск потока где–то должен случиться явно. Даже await–вызовы начинаются синхронно и уже внутри тем или иным методом инициируют асинхронность.
S>Что значит "рубят на физическом уровне"?
Когда поток срубается дотнетом, в нем эмулируется исключение ThreadAbortException. Но есши потгк создан и контролируется анменеджед–кодом он может быть срублен вызовом TerminateThread. При этом эмулировать генерацию ThreadAbortException будет некому и отряд просто не заметит потери бойца.
S>Так есть же в цикле readResult = await bodyReader.ReadAsync();
Откровенно говоря не заметил await в первый раз. Подсветка подвела.
Очевидно он начинается на исходном потоке или блокирует его. Плюс там возможен захват контекста и попытка продолжиться на нем же (предположение).
Все эти async–и в дотнете сделаны кривовато. Слишком много не явного (не явные глобальные переменные контекста, не внятная семантика асинхронности).
Люди свято верят, если метод async, то код уже в другом потоке. Но это не так. Где–то асинхронность должна инициироватся явно. А то, что метод с async это делает, не более чем соглашение. Это надо держать в уме.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
VD>Люди свято верят, если метод async, то код уже в другом потоке. Но это не так. Где–то асинхронность должна инициироватся явно. А то, что метод с async это делает, не более чем соглашение. Это надо держать в уме.
MoveNext()
Перемещает конечный автомат в его следующее состояние.
SetStateMachine(IAsyncStateMachine)
Настраивает конечный автомат с размещенной в куче репликой.
и солнце б утром не вставало, когда бы не было меня
Это вообще не о том. Эта статья о том как режется метод на части с продолжениями. Но никто не гарантирует, что вызов с async прямо перемещается на другой поток.
Вот этот Task.Delay(1000) из статьи, например, создает внутри себя System.Threading.Timer с интервалом 1000 миллисекунд и задает ему делегат, который вызывает Task.Complete(). Вот от этого таймера и появляется асинхронность, так как события таймера вызываются на потоках из пула. А если бы Task.Delay() тупо закомплитил задачу прямо в своем теле, то никаких потоков не создалось бы.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Вот этот Task.Delay(1000) из статьи, например, создает внутри себя System.Threading.Timer с интервалом 1000 миллисекунд и задает ему делегат, который вызывает Task.Complete(). Вот от этого таймера и появляется асинхронность, так как события таймера вызываются на потоках из пула. А если бы Task.Delay() тупо закомплитил задачу прямо в своем теле, то никаких потоков не создалось бы.
Это ты описал работу TaskCompletionSource
В статье
AsyncTaskMethodBuilder(<>t__builder) — представляет построитель для асинхронных методов, которые возвращают задачу. Этот вспомогательный тип и его члены предназначены для использования компилятором. Здесь инкапсулирована логика, общая для всех конечных автоматов. Именно этот тип создает объект Task, возвращаемых заглушкой. На самом деле этот тип очень похож на класс TaskCompletionSource в том смысле, что он создает задачу-марионетку, которую можно сделать завершенной позже. Отличие от TaskCompletionSource заключается в том, что AsyncTaskMethodBuilder оптимизирован для async-методов и ради повышения производительности является структурой, а не классом.
TaskAwaiter(<>u_awaiter) — здесь хранится временный объект, который ожидает завершения асинхронной задачи. Также представлен в виде структуры и помогает оператору await подписаться на уведомление о завершении задачи Task.
Суть в
В процедуре подписки на уведомление участвует также объект AwaitUnsafeOnCompleted. Именно здесь реализуются дополнительные возможности await, в том числе запоминание контекста синхронизации, который нужно будет восстановить при возобновлении. Этот метод планирует конечный автомат для перехода к следующему действию по завершении выполнения указанного объекта типа awaiter. В качестве параметров: AsyncTaskMethodBuilder.AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine). Как видно перед вызовом этого метода происходит загрузка в стек двух переменных с индексом 2 и 3, где 2 — valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, 3 — class Asynchronous.Program/'d__0'
Рассмотрим немного подробнее структуру AsyncTaskMethodBuilder(сильно глубоко копать здесь не буду, потому что на мой взгляд изучение этой структуры и все, что с ней связано можно расписать как бы не на несколько статей):
/// <summary>Кэшированная задача для default(TResult).</summary>
internal readonly static Task<TResult> s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult));
/// <summary>Состояние, связанное с IAsyncStateMachine.</summary>private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly
/// <summary>Ленивая инициализация задачи</summary>private Task<TResult> m_task; // lazily-initialized: must not be readonly
/// <summary>
/// Планирует состояние данной машины для дальнейшего действия, когда awaiter выполнится /// </summary>
/// <typeparam name="TAwaiter">Определяет тип awaiter.</typeparam>
/// <typeparam name="TStateMachine">Определяет тип состояния машины.</typeparam>
/// <param name="awaiter">The awaiter.</param>
/// <param name="stateMachine">Состояние машины.</param>
[SecuritySafeCritical]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
try
{
AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null;
var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize);
// Если это первый await, то мы не упаковали состояние машины и должны сделать это сейчасif (m_coreState.m_stateMachine == null)
{
// Действие задачи должно быть проинициализировано до первого приостановления var builtTask = this.Task;
//Упаковка состояния машины при помощи вызова internal-метода,
// где ссылка будет храниться в кеше.
m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, builtTask);
}
awaiter.UnsafeOnCompleted(continuation);
}
catch (Exception e)
{
AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null);
}
}
Рассмотрим вкратце, что внутри этой структуры:
s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)) — здесь создается задача без реализации Dispose при указании специального флага DoNotDispose. Данный подход используется при создании задач для кеширования или повторного использования.
AsyncMethodBuilderCore m_coreState — представляет состояние, связанное с выполнением IAsyncStateMachine. Это структура.
AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize — предоставляет возможность вызова метода конечного автомата MoveNext согласно предоставленному контексту выполнения программы. Это структура, которая содержит контекст выполнения, состояние конечного автомата и метод для выполнения MoveNext.
m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn? this.Task: null, ref runnerToInitialize) — получает Action при ожидании метода UnsafeOnCompleted и происходит прикрепление как задачи продолжения. Здесь также происходит запоминание состояния машины и контекста выполнения.
awaiter.UnsafeOnCompleted(continuation) — планирует продолжение действий, которые будут вызваны, когда экземпляр завершит выполнение. При этом в зависимости от того, нужно ли нам восстанавливать контекст или нет, будет вызван метод MoveNext соответственно с контекстом да приостановки метода или же выполнение продолжится в контексте того потока, в котором происходило выполнение задачи.
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, Serginio1, Вы писали:
S> Это ты описал работу TaskCompletionSource
Нет. Это я описал общую логику async. Потоки ни от куда не берутся. Они всегда создаются явно. К самому механизму async это отношения не имеет. Автомат там почти точно такой же, как и в энумераторах с той лишь разницей, что вместо возврата последовательности тут получается последовательность вызовов. Вместо КА могли бы быть монады. Кстати, они сильно универсальнее.
ЗЫ
Пожалуйста, цитируй только то, что нужно для понимания твоего ответа. Не вынуждай банить.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Немного утрировано:
* PostRequest получит управление и начнет выполнять свой код
* Дойдет до context.SetBody();
* Внутри context.SetBody(); дойдет до readResult = await bodyReader.ReadAsync();
* Тут начнется асихронное выполнение ReadAsync, поскольку await выше нет, то управление вернется PostRequest
* PostRequest продолжит свою работу и если "фоном" SetBody не успел сделать свою работу, то значит результата и не будет.
Лечить как уже сказали: await context.SetBody(); + public async Task SetBody()
И запсать себе что "async void" в 99.9% это антипаттерн и делать так можно если на 100% уверен и понимаешь что происходит.
В оставшемся 0.1% это реализация fire-and-forget, когда мы что-то запустили в работу но нам без разница когда это закончится и успешно ли вообще это закончится или упадет.
Здравствуйте, VladD2, Вы писали:
VD>Нет. Это я описал общую логику async. Потоки ни от куда не берутся. Они всегда создаются явно. К самому механизму async это отношения не имеет. Автомат там почти точно такой же, как и в энумераторах с той лишь разницей, что вместо возврата последовательности тут получается последовательность вызовов. Вместо КА могли бы быть монады. Кстати, они сильно универсальнее.
Я все к тому, что вся кухня async это довольно сложный механизм. Там же весь async может происходить в одном потоке например при вызове Task.FromResult или
или тот же TaskCompletionSource создан без RunContinuationsAsynchronously то продолжение будет в том же потоке, что и вызван SetResult.
Тут не надо верить, нужно разбираться в тонкостях.
Ну и опять же, потоки явно не создаются а берутся уже из пула потоков или выполняются в потоке синхронизации или в том же потоке.
Я к тому, что нужно давать более точный ответ. Это полезно для задающего вопрос.
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, VladD2, Вы писали:
VD>Когда поток срубается дотнетом, в нем эмулируется исключение ThreadAbortException.
В Core оно так не работает.
VD>Откровенно говоря не заметил await в первый раз. Подсветка подвела.
В старом ASP.NET, который в FW, контекст синхронизации не создает потоки, весь асинхронный код в рамках одного запроса выполняется в одном потоке. Причем, в отличие от винформсов, там еще и message loop с пробросом коллбеков через него отсутствует.
В ASP.NET Core контекст синхронизации дефолтный, т.е. использует для асинхронных операций потоки из пула.
VD>Люди свято верят, если метод async, то код уже в другом потоке. Но это не так. Где–то асинхронность должна инициироватся явно.
Не обязательно. Все зависит от контекста синхронизации.