Дан код на C#: async void Save(){ await db.SaveAsync(); throw new Exception("fail"); } — объясните проблему использования async void, как правильно обрабатывать исключения в асинхронных методах и какие паттерны для resiliency применять

6 Фев в 15:34
49 +1
0
Ответы
1
Проблема
- `async void` нельзя дождаться и нельзя корректно обработать исключение у вызывающего: исключение будет брошено в контекст синхронизации (или в пул задач) и не попадёт в `await` у вызывающего — часто приводит к непредсказуемому завершению приложения или «потерянным» ошибкам. `async void` допустим только для обработчиков событий (event handlers).
Как правильно — обработка исключений
1. Меняйте сигнатуру на `async Task` (или `Task`), чтобы исключения корректно проксировались и могли быть пойманы:
```
async Task SaveAsync()
{
await db.SaveAsync();
throw new Exception("fail");
}
// у вызывающего:
try
{
await SaveAsync();
}
catch (Exception ex)
{
// обработка/логирование
}
```
2. Если нужен fire-and-forget — не используйте `async void`. Делайте:
- запуск в фоне с сохранением и обработкой ошибки:
```
_ = SaveAsync().ContinueWith(t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
```
- или очередь фоновых задач / IHostedService / BackgroundService / внешняя очередь (Hangfire, Azure Queue) — это надёжнее.
3. Для обработчиков событий допускается `async void`, но обязательно локальный `try/catch`:
```
async void Button_Click(object s, EventArgs e)
{
try
{
await SaveAsync();
}
catch (Exception ex)
{
// показать ошибку пользователю/залогировать
}
}
```
4. Всегда поддерживайте `CancellationToken` для отмены и используйте `ConfigureAwait(false)` в библиотечных методах при необходимости.
Resiliency — шаблоны и практики
- Retry (повторы) с экспоненциальной задержкой:
- пример с Polly:
```
var retry = Policy
.Handle()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(111),
TimeSpan.FromSeconds(222),
TimeSpan.FromSeconds(444)
}, (ex, ts) => Log($"retry after {ts} due to {ex}"));
await retry.ExecuteAsync(() => db.SaveAsync());
```
- Circuit Breaker — разрыв при повторных ошибках, чтобы не перегружать зависимость:
```
var cb = Policy
.Handle()
.CircuitBreakerAsync(555, TimeSpan.FromSeconds(303030));
```
- Timeout — ограничить время ожидания операции:
```
var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(101010));
```
- Bulkhead (изоляция) — ограничение параллельных вызовов для защиты ресурсов.
- Fallback — запасной путь (например, записать в локальную очередь/кеш, если основной save упал).
- Комбинация политик (wrap): retry + circuit breaker + timeout:
```
var policy = Policy.WrapAsync(retry, cb, timeout);
await policy.ExecuteAsync(() => db.SaveAsync());
```
- Метрики и логирование — счётчики ошибок, время ответа, трейсинг.
- Надёжные фоновые обработчики — для fire-and-forget используйте очередь + длительно работающий воркер (IHostedService), где ошибки контролируются и повторяются безопасно.
Короткие рекомендации
- Избегайте `async void` (кроме событий). Используйте `Task`/`Task`.
- Обрабатывайте исключения либо внутри метода (если локально нужно), либо на уровне вызывающего (`try/await`).
- Для сетевых/внешних операций применяйте retry, circuit breaker, timeout, bulkhead и fallback (можно взять Polly).
- Для фоновых задач используйте надёжную очередь + воркер вместо fire-and-forget.
Это позволит корректно ловить ошибки, делать повторы и защищать приложение от лавинообразных отказов.
6 Фев в 15:48
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир