Как работает этот метод многопоточной записи в файл?

Существует условный асинхронный метод, который записывает в файл юзера написанный этим юзером текст (реализовано через телеграмм бота). Вопрос таков: при одновременном вводе текста от нескольких пользователей как осуществляется работа данного метода, т.е. в нём самом идёт последовательное обращение к каждому файлу, как к потоку и соответственно идёт просто последовательная запись, либо же идёт обращение сразу ко всем нужным файлам через несколько потоков (если их количество не выходит за рамки ограничений)?

Вот неполный код самого метода:

static public async Task NewPostAsync(string messageText, string filePath)
        {
            await Task.Run(() =>
            {
                using (var sw = new StreamWriter(filePath, true))
                {
                    sw.WriteLine(messageText);
                }
            });

Файлы хранятся вот так:

string path = $@"...\project\users\{message.Chat.Id}.txt";

Очень интересно узнать у знающих как это происходит и нужно ли при такой задаче использовать подобное, либо же можно использовать что-то более простое.


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

Автор решения: Вадим

Очень краткая инфа по таскам) Существует так называемый пулл потоков, в котором система создает и прихранивает определенное количество потоков. Таска берет свободный поток из пула, и выполняет там задачу (иногда их не хватает, но в целом можно считать что каждая таска работает в отдельном потоке). Итого при получения сообщения, у вас берется поток, в нем открывается файл, в него пишутся данные, файл закрывается, поток возвращается в пул.

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

Плюс есть ограничения файловой системы на одновременную запись.

На вскидку с реализацией, с которой было бы проще (сугубо с моей точки зрения), сделать очередь. И два потока. Первый получает данные от сети, и кладет в очередь команду: "Записать такой то тест в такой то файл". Второй поток из этой очереди выгребает данные, и пишет файлы. В этом случае вы с одной стороны не блокируете программу на операцию записи файла. С другой стороны вы гарантируете что все файлы будут записаны в том порядке что они пришли, и без конфликтов.

→ Ссылка
Автор решения: CrazyElf

Так это зависит от того, как вы обращаетесь к функции NewPostAsync. Если у вас есть какой-то цикл, который перебирает последовательно какие-то сущности и обращается внутри этого цикла к async методам, в том числе делает await NewPostAsync то обращение будет последовательное. А если взять другой крайний метод - вы собираете каким-то образом все такие обращения к NewPostAsync в один массив и делаете потом Task.WhenAll от этого массива, то тогда обращения к этим функциям будут точно идти параллельно, но, конечно, в рамках наличия свободных потоков. Между этими двумя крайностями возможны разные варианты, надо смотреть какая у вас архитектура. В целом, такой подход правилен и хорош, если в основном предполагается, что запись идёт в разные файлы практически одновременно. А вот если запись может быть и в один и тот же файл, то я бы предусмотрел некий механизм блокировки по имени файла, например, в виде ConcurrentBag, проверял бы на наличие в нём имени файла. Но там надо ещё думать, как это всё нормально сделать. Может быть лучше скрестить с методом, предложенным в другом ответе - чтобы была очередь, которую разгребает один поток и вот этот поток чтобы с одной стороны писал в файлы асинхронно, через Task-и, чтобы была параллельность, а с другой стороны - чтобы он проверял, не пишется ли в данный момент уже какой-то файл и пропускал бы такой файл в очереди, пока он не освободится в итоге.

→ Ссылка
Автор решения: aepot

Это просто асинхронная запись в файл, написанная очень старым программистом, который не в курсе про актуальные файловые API.

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

Проще написать конечно можно.

public static async Task NewPostAsync(string messageText, string filePath)
{
    await File.AppendAllLinesAsync(filePath, new[] { messageText });
}

А еще можно оптимизировать без лишней генерации машины состояний (state machine (англ.) - конечный автомат), которая генерируется компилятором, когда используется ключевое слово async.

public static Task NewPostAsync(string messageText, string filePath)
{
    return File.AppendAllLinesAsync(filePath, new[] { messageText });
}

Или с модным выражением вместо код-блока.

public static Task NewPostAsync(string messageText, string filePath)
    => File.AppendAllLinesAsync(filePath, new[] { messageText });

Такой способ избавления от async/await валиден только когда в коде один await и он является последним в методе, то есть после него нет кода. И я бы даже рекомендовал при возможности его использовать.

А еще можно с синхронизацией, чтобы только одна задача могла писать на диск одновременно. Еще раз напомню, что асинхронная запись здесь сделана не для того, чтобы "быстрее записывать", а для того, чтобы основной код бота продолжал работать с сетевыми запросами в тот момент, когда эта запись идет, а не ждал ее завершения.

private static readonly SemaphoreSlim writeFileSemaphore = new SemaphoreSlim(0, 1);

static public async Task NewPostAsync(string messageText, string filePath)
{
    await writeFileSemaphore.WaitAsync().ConfigureAwait(false);
    await File.AppendAllLinesAsync(filePath, new[] { messageText });
    writeFileSemaphore.Release();
}

Можно еще дальше издеваться над кодом: добавить очередь для записи, например, если запись на диск уже идет, то вместо ожидания, можно закинуть запрос на запись в очередь, например ConcurrentQueue, а затем считывать очередь, когда текущая запись завершится. Далее можно выбирать из очереди сообщения для записи в один и тот же файл пачками и писать в одну операцию на диск сразу все что накопилось. Можно создать в отдельном потоке класс-записыватель логов, который будет принимать запросы через BlockingCollection, или через модный Channel. В общем, простор для творчества огромный.

Подробнее об асинхронном программировании можно почитать здесь.

→ Ссылка