Как работает этот метод многопоточной записи в файл?
Существует условный асинхронный метод, который записывает в файл юзера написанный этим юзером текст (реализовано через телеграмм бота). Вопрос таков: при одновременном вводе текста от нескольких пользователей как осуществляется работа данного метода, т.е. в нём самом идёт последовательное обращение к каждому файлу, как к потоку и соответственно идёт просто последовательная запись, либо же идёт обращение сразу ко всем нужным файлам через несколько потоков (если их количество не выходит за рамки ограничений)?
Вот неполный код самого метода:
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 шт):
Очень краткая инфа по таскам) Существует так называемый пулл потоков, в котором система создает и прихранивает определенное количество потоков. Таска берет свободный поток из пула, и выполняет там задачу (иногда их не хватает, но в целом можно считать что каждая таска работает в отдельном потоке). Итого при получения сообщения, у вас берется поток, в нем открывается файл, в него пишутся данные, файл закрывается, поток возвращается в пул.
Одна из самых распространенных проблем такой архитектуры. Если, вдруг, у вас два пользователя захотят записать данные в один и тот же файл.... Система упадет. Потому что файл открыт одним потоком, и с другого в этот момент запись невозможна.
Плюс есть ограничения файловой системы на одновременную запись.
На вскидку с реализацией, с которой было бы проще (сугубо с моей точки зрения), сделать очередь. И два потока. Первый получает данные от сети, и кладет в очередь команду: "Записать такой то тест в такой то файл". Второй поток из этой очереди выгребает данные, и пишет файлы. В этом случае вы с одной стороны не блокируете программу на операцию записи файла. С другой стороны вы гарантируете что все файлы будут записаны в том порядке что они пришли, и без конфликтов.
Так это зависит от того, как вы обращаетесь к функции NewPostAsync. Если у вас есть какой-то цикл, который перебирает последовательно какие-то сущности и обращается внутри этого цикла к async методам, в том числе делает await NewPostAsync то обращение будет последовательное.
А если взять другой крайний метод - вы собираете каким-то образом все такие обращения к NewPostAsync в один массив и делаете потом Task.WhenAll от этого массива, то тогда обращения к этим функциям будут точно идти параллельно, но, конечно, в рамках наличия свободных потоков.
Между этими двумя крайностями возможны разные варианты, надо смотреть какая у вас архитектура. В целом, такой подход правилен и хорош, если в основном предполагается, что запись идёт в разные файлы практически одновременно. А вот если запись может быть и в один и тот же файл, то я бы предусмотрел некий механизм блокировки по имени файла, например, в виде ConcurrentBag, проверял бы на наличие в нём имени файла. Но там надо ещё думать, как это всё нормально сделать. Может быть лучше скрестить с методом, предложенным в другом ответе - чтобы была очередь, которую разгребает один поток и вот этот поток чтобы с одной стороны писал в файлы асинхронно, через Task-и, чтобы была параллельность, а с другой стороны - чтобы он проверял, не пишется ли в данный момент уже какой-то файл и пропускал бы такой файл в очереди, пока он не освободится в итоге.
Это просто асинхронная запись в файл, написанная очень старым программистом, который не в курсе про актуальные файловые 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. В общем, простор для творчества огромный.
Подробнее об асинхронном программировании можно почитать здесь.