Асинхронная запись в файл (c#)
Занимаюсь написанием ТГ-бота, есть асинхронный метод, который обрабатывает сообщения пользователей и, если пользователь зашел в бота впервые, записывает данные его ТГ-аккаунта в .json файл.
Подозреваю, что возможен случай одновременного обращения к файлу при одновременном обращении новых пользователей в бота, соответственно два потока будут пытаться писать в один файл
Подскажите как можно исключить данную ситуацию?
public static async Task UpdateUserFile(string jsonPath, List<User> userList)
{
try
{
var jsonString = System.Text.Json.JsonSerializer.Serialize(userList);
await System.IO.File.WriteAllTextAsync(jsonPath, jsonString);
}
catch (Exception ex)
{
///...
}
}
Ответы (2 шт):
Можно использовать SemaphoreSlim
для синхронизации доступа к ресурсу.
Например, так:
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public static async Task UpdateUserFile(string jsonPath, List<User> userList)
{
await semaphore.WaitAsync();
try
{
var jsonString = System.Text.Json.JsonSerializer.Serialize(userList);
await System.IO.File.WriteAllTextAsync(jsonPath, jsonString);
}
catch (Exception ex)
{
// Обработка исключений
}
finally
{
semaphore.Release();
}
}
Я уже отвечал на похожий вопрос.
Возьму код оттуда и изменю под вашу задачу:
private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
private static SemaphoreSlim GetLock(string path)
{
SemaphoreSlim semaphore;
while (!_locks.TryGetValue(path, out semaphore))
{
semaphore = new SemaphoreSlim(1);
if (_locks.TryAdd(path, semaphore))
break;
}
return semaphore;
}
public static async Task ConcurrentSaveAsync(string path, string text, CancellationToken token = CancellationToken.None)
{
SemaphoreSlim semaphore = GetLock(path);
await semaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await File.WriteAllTextAsync(path, text, token).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
Здесь получается, что каждый записываемый файл будет контролироваться своим семафором. Следовательно, одновременный доступ к разным файлам будет возможен без ожидания.
И то же самое для загрузки, иначе можно попасть в ситуацию, когда будет происходить чтение во время записи:
public static async Task<string> ConcurrentLoadAsync(string path, CancellationToken token = CancellationToken.None)
{
SemaphoreSlim semaphore = GetLock(path);
await semaphore.WaitAsync(token).ConfigureAwait(false);
try
{
return await File.ReadAllTextAsync(path, token).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
Ну и наконец ваш метод:
public static async Task UpdateUserFile(string jsonPath, List<User> userList)
{
try
{
var jsonString = System.Text.Json.JsonSerializer.Serialize(userList);
await ConcurrentSaveAsync(jsonPath, jsonString);
}
catch (Exception ex)
{
//...
}
}
Теперь просто пишите и читайте файлы, к которым возможен многопоточный доступ с помощью методов ConcurrentSaveAsync
и ConcurrentLoadAsync
.
Единственный минус решения в том, что если файлов будет создаваться условно бесконечное множество, то семафоры будут накапливаться в словаре. В этом случае надо будет подумать о его очистке, но эта задача непростая. Если же файлов ограниченное количество, то всё и так сработает нормально.
На самом деле всё, что выше — костыли, хоть и рабочие. В реальности некостыльное решение тоже есть — использовать базу данных.