правильное использование ассинхронности

Подскажите, пожалуйста, какой из методов более правильный и почему? У меня в синхронном цикле перебирается 5000 объектов. Сначала идут вычисления, затем действия по сети и в конце запись в текстовый лог.

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

    void Log(string text)
    {
        _ = File.AppendAllTextAsync($"\\logs\\{mail}", text);
    }

    void Log2(string text)
    {
        Task.Run(()=>File.AppendAllText($"\\logs\\{mail}", text));
    }

    async Task Log3(string text)
    {
        await File.AppendAllTextAsync($"\\logs\\{mail}", text);
    }

Благодарю заранее за любые подсказки.


Ответы (1 шт):

Автор решения: aepot
async Task Log3(string text)
{
    await File.AppendAllTextAsync($"\\logs\\{mail}", text);
}

А можно ли вызывать его не как await Log3(text), а просто _ = Log3(text)?

Вызвать то конечно можно, вам нужен режим запуска "запустил и забыл", но что будет с файлом, если одновременно вы вызвали этот метод, скажем, 100 раз? Возникнет исключение при поытке открыть на запись уже открытый файл, которое просто пропадет в бездну, уронит поток, и всё, ну потому что вы его никак не обрабатываете и никак не синхронизируете доступ к файлу из разных потоков. Выглядеть это будет как в лог что-то записалось, но не всё, что вы хотели туда записать. Плохой эффект.

Что же делать - нужно использовать блокировку. Вот например решение для многопоточного доступа без асинхронности.

Для вашего частного случая, можно поступить следующим образом - правильно использовать async void.

static readonly SemaphoreSlim _logSemaphore = new SemaphoreSlim(1);

async void Log4(string text)
{
    await _logSemaphore.WaitAsync();
    try
    {
        // только один поток в один момент времени может попасть в эту часть кода
        await File.AppendAllTextAsync($"\\logs\\{mail}", text);
    }
    catch (Exception ex)
    {
        // ... ex.Message - ошибка при записи в лог
    }
    finally
    {
        _logSemaphore.Release();
    }
}

Тогда можно просто вызвать

Log4("text");

И сообщение отправится в лог без ожидания окончания записи. Но как сказано выше в комментариях - лучше использовать готовые логгеры, а не изобретать свой.

Дисклеймер

Как вы и хотели, я сделал без ожидания, но есть побочный эффект, например идет запись в лог, и вы в этот момент закрываете прилижение. Вас устроит недозаписанный лог? Или падение приложения при закрытии? Или отсутствие последней записи в логе? Как вы дождетесь окончания записи в лог? Никак. Потому что async void нельзя ожидать. Рассмотрите это решение как пример и попробуйте его доработать так, чтобы ситуации, когда приложение закрылось недозаписать все логи, не возникло.

async void - опасно, плохо, и я не рекомендую это использовать без полного понимания, как это работает. _ = Log3 дает тот же самый эффект, что async void, то есть та же проблема может возникнуть.

Куда копать - шаблон проектирования Producer/Consumer и подходящая вам его реализация.

→ Ссылка