Проблема с асинхронным методом C# с запуском и забыванием

Вопрос или проблема

Асинхронный метод Like должен выполнять действие “нравится” от пользователя с помощью принца “отправил и забыл”. Но поскольку я задаюсь вопросом, как лучше всего обработать асинхронный “отправил и забыл”.

Вариант 1:

public async Task Like(LikeDto dto)
{
    var _ = svc.ExecuteDb(async (context) =>
    {
        var (rating, liked) = await likeAndSave(context, dto.Liked);
        await sendEvents(dto.Id, liked);
    });
   
    await Task.CompletedTask;
}

Вариант 2:

public async Task Like(LikeDto dto)
{
    var _ = svc.ExecuteDb(async (context) =>
    {
        var (rating, liked) = await likeAndSave(context, dto.Liked);
        await sendEvents(dto.Id, liked);
    });

    await Task.FromResult(_);
}

Вариант 3:

public async Task Like(LikeDto dto)
{
      await Task.Run(() =>
      {
          var _ = svc.ExecuteDb(async (context) =>
          {
              var (rating, liked) = await likeAndSave(context, dto.Liked);
              await sendEvents(dto.Id, liked);
          });
      });
}

Класс svc для помощи, когда задача не ожидается, и контекст http-запроса для инжектируемых сервисов не будет доступен.

public class SercicesProvider<T> where T : DbContext
{
    private readonly IServiceProvider _services;

    public SercicesProvider(IServiceProvider svc)
    {
        _services = svc;
    }

    public async Task ExecuteDb(Func<T, Task> action)
    {
        using (var scope = _services.CreateScope())
        {
            var service = scope.ServiceProvider.GetRequiredService<T>();
            await action(service);
        }
    }
}

Все подходы работают, но await Task.CompletedTask; кажется, является наилучшим способом обработки.

Ответ или решение

Решение проблемы с асинхронными методами C#: Fire and Forget

Вопрос, который вы подняли, касается реализации асинхронного метода «Like» в языке C#, который нужно выполнить в режиме «fire and forget» (сделай и забудь). Поскольку ваша цель заключается в том, чтобы отправлять действия пользователей без ожидания завершения их выполнения, давайте проанализируем предложенные варианты и рассмотрим лучший подход к реализации такой логики.

Введение в проблему

Методы, которые используются для работы с асинхронным программированием в C#, дают разработчикам возможность эффективно управлять задачами, которые могут занимать значительное время выполнения, как, например, операции ввода-вывода. Проблема «fire and forget» заключается в том, что необходимо управлять выполнением этих задач, не ожидая их завершения, что может привести к проблемам с обработкой ошибок или утечкой контекста.

Анализ предложенных решений

Опция 1
public async Task Like(LikeDto dto)
{
    var _ = svc.ExecuteDb(async (context) =>
    {
        var (rating, liked) = await likeAndSave(context, dto.Liked);
        await sendEvents(dto.Id, liked);
    });

    await Task.CompletedTask;
}

В этой реализации используется Task.CompletedTask, который является хорошей практикой для того, чтобы метод завершался быстро, однако эта опция не гарантирует выполнение задачи, так как запускаемый код внутри ExecuteDb не привязан к завершению метода Like.

Опция 2
public async Task Like(LikeDto dto)
{
    var _ = svc.ExecuteDb(async (context) =>
    {
        var (rating, liked) = await likeAndSave(context, dto.Liked);
        await sendEvents(dto.Id, liked);
    });

    await Task.FromResult(_);
}

Вариант использует Task.FromResult(_);, но это также не придаёт никаких гарантий, что выполнение задач завершено. Но важно понимать, что FromResult возвращает уже завершенный результат, что не самым наилучшим образом выполняет концепцию «fire and forget».

Опция 3
public async Task Like(LikeDto dto)
{
    await Task.Run(() =>
    {
        var _ = svc.ExecuteDb(async (context) =>
        {
            var (rating, liked) = await likeAndSave(context, dto.Liked);
            await sendEvents(dto.Id, liked);
        });
    });
}

Вызывая Task.Run, вы помещаете асинхронный метод в пул потоков, но это не идеальное решение при работе с асинхронными методами, поскольку это может привести к нежелательным последствиям, связанным с многопоточностью и управлением контекстом.

Лучший подход

Из представленных решений для обработки концепции «fire and forget» наиболее уместным будет использование ExecuteDb, не ожидая завершения задачи напрямую в методе Like. Однако, чтобы должным образом обрабатывать возможные исключения и утечки контекста, следует скорректировать подход, возможно, с использованием фонового сервиса или обработки ошибок внутри ExecuteDb.

Вот пример улучшенной реализации:

public Task Like(LikeDto dto)
{
    _ = svc.ExecuteDb(async (context) =>
    {
        try
        {
            var (rating, liked) = await likeAndSave(context, dto.Liked);
            await sendEvents(dto.Id, liked);
        }
        catch (Exception ex)
        {
            // Логирование ошибки
        }
    });

    return Task.CompletedTask; // Возвращаем завершённую задачу
}

Такой подход позволяет выполнять действия асинхронно, не зависимо от контекста метода Like, а также обрабатывать возможные ошибки.

Заключение

В контексте асинхронного программирования в C# выбор подхода «fire and forget» требует серьезного анализа. Важно не только организовать запуск фоновых задач, но и обеспечить надежность обработки. Вам необходимо учитывать принципы управления жизненным циклом сервисов в ASP.NET и возможные утечки контекста.

Для оптимальной обработки задач рекомендуется использовать подходы, которые обеспечивают надежное выполнение кодов, с возможностью обработки ошибок. Понимание этих принципов позволит формировать устойчивую систему, способную справляться с вызовами асинхронного программирования.

Оцените материал
Добавить комментарий

Капча загружается...