Это происходит потому, что в вашем рабочем процессе нет ничего асинхронного, когда Sleep
закомментирован.
Все полностью синхронно, но поскольку оно закодировано в вычислительном выражении async
, оно становится странно вложенным.Видите, каждая строка let!
на самом деле вызывает все, что находится справа (в вашем примере - workAsync
) и передает ей обратный вызов, который должен быть вызван после выполнения асинхронной части.Обратный вызов содержит остальную часть кода - продолжение, начинающееся сразу после строки let!
.Компилятор выполняет умные преобразования кода, чтобы он выглядел все красиво и линейно, хотя на самом деле это серия обратных вызовов.
Однако, поскольку workAsync
на самом деле не асинхронный, он просто вызывает обратный вызов сразуи обратный вызов оборачивается и вызывает следующую итерацию workAsync
и так далее.И так ваш стек растет.
Но подождите!Это на самом деле не должно расти в конце концов.Вызов обратного вызова является последним вызовом в workAsync
, также известном как «хвостовой вызов», и и .NETCore, и .NET Framework их устраняют (и действительно: на моем компьютере я не могу воспроизвести ваш результат).Единственное предположение, которое я могу предложить, это то, что вы должны запускать это на Mono, что не всегда устраняет хвостовые вызовы.
Однако, если вы раскомментируете Sleep
, это становится переломным моментом.Sleep
является на самом деле асинхронным, что означает, что он планирует выполнение обратного вызова в новом потоке после истечения времени ожидания.Это выполнение начинается с нуля, со свежим стеком, и поэтому стек не растет, даже когда хвостовые вызовы не исключены.
Итак, чтобы ответить на ваш первоначальный вопрос: нет, нетбесконечное асинхронное вычисление не может переполнить стек, кроме случаев, когда оно фактически не асинхронно и выполняется в Mono.