Асинхронная запись в файл (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 шт):

Автор решения: Chaos_Sower

Можно использовать 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();
    }
}
→ Ссылка
Автор решения: aepot

Я уже отвечал на похожий вопрос.

Возьму код оттуда и изменю под вашу задачу:

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.

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


На самом деле всё, что выше — костыли, хоть и рабочие. В реальности некостыльное решение тоже есть — использовать базу данных.

→ Ссылка