Утечка памяти, которую не устраняет GC в отношении .Net Entity Framework версии 8 (консольное приложение C# .Net8.02)

Для большинства вещей, которые я пишу, меня обычно не волнует использование памяти, однако у меня есть консольное приложение, написанное на .Net8.02 с использованием EF8, которое теряет 2 МБ каждый раз при вызове транзакции EF. Я испробовал все мыслимые варианты использования GC (сборщика мусора), пытаясь заставить .Net освобождать память между вызовами, но утечка сохраняется. Я также разместил фрагменты кода во всех местах для отслеживания использования памяти, и все это возвращается в EF. 2 МБ может показаться не таким уж большим, но ежедневно эта программа считывает около 5000 файлов журналов (или более), и программа приводит к сбою машины (VM или реальной) - я написал монитор памяти с GC, чтобы посмотреть доступную память, а затем остановиться обработки и подождите, пока он снова не отключится (это никогда не происходит!).

В любом случае, именно в этом и заключается проблема (LogFileProcessor.cs). Если у кого-нибудь есть идеи о том, как освободить память, которую поглощает EF, я был бы очень признателен (прежде чем я вернусь и буду использовать ADO.Net Core (у которого нет такой же проблемы - разберитесь)

 public partial class LogFileProcessor(LogDbContext dbContext, ILogger<LogFileProcessor> logger)
 {
     private readonly LogDbContext _dbContext = dbContext;
     private readonly ILogger<LogFileProcessor> _logger = logger;

     private List<LogEntry> _logEntries = new List<LogEntry>();
     private string[] _lines = [];

     public async Task<bool> ProcessLogFileAsync(string filePath)
     {
         bool result = false;

         if (!File.Exists(filePath))
         {
             _logger.LogError("File not found: {filePath}", filePath);
             return result;
         }

         string fileName = Path.GetFileName(filePath);

         if (await LogAlreadyProcessedAsync(fileName))
         {
             _logger.LogInformation("Log file already processed: {fileName}", fileName);
             return result;
         }

         string fileNameNoExt = Path.GetFileNameWithoutExtension(filePath);
         DateTime fileDate = File.GetLastWriteTime(filePath);
         string fileHash = ProgramBase.ComputeSha256Hash(filePath);
         int logFileId = ExtractLogFileId(fileNameNoExt);
         string fileType = ExtractFileType(fileNameNoExt);

         using var transaction = await _dbContext.Database.BeginTransactionAsync();
         try
         {
             var parsedLog = new ParsedLog
             {
                 FileName = fileName,
                 LogType = fileType,
                 LogFileId = logFileId,
                 DateParsed = DateTime.UtcNow,
                 FileDate = fileDate,
                 FileHash = fileHash
             };

             await _dbContext.ParsedLogs.AddAsync(parsedLog);
             await _dbContext.SaveChangesAsync();

             int parsedLogId = parsedLog.Id; //retrieve new Id (identity) from ParsedLogs table

             //_lines = await File.ReadLinesAsync(filePath).ToArray();   //not really needed, but if user sets log file size really large, this is better for resources
             _lines = await File.ReadAllLinesAsync(filePath);
             int lineNum = 0;
             foreach (var line in _lines)
             {
                 var entry = ParseLine(line, parsedLogId, lineNum);
                 if (entry != null)
                 {
                     _logEntries.Add(entry);
                 }
                 else
                 {
                     throw new Exception($"Unable to parse or convert line {lineNum}");
                 }
                 lineNum += 1;
             }

             await _dbContext.LogEntries.AddRangeAsync(_logEntries);
             await _dbContext.SaveChangesAsync();

             await transaction.CommitAsync();
             _logger.LogInformation("Log file: {fileName} processed and data committed to the database.", fileName);
             await transaction.DisposeAsync();
             result = true;
         }
         catch (Exception ex)
         {
             await transaction.RollbackAsync();
             _logger.LogError("Error processing log file: {fileName} {ex.Message}", fileName, ex.Message);
             await transaction.DisposeAsync();
             result = false;
         }
         finally
         {
             _logEntries.Clear();
             _lines = [];

             // Force garbage collection - naturally, this doesn't work, UGH!
             GC.Collect();
             GC.WaitForPendingFinalizers();
             GC.Collect();
         }
         return result;
     }



     private async Task<bool> LogAlreadyProcessedAsync(string fileName)
     {
         return await _dbContext.ParsedLogs.AsNoTracking().AnyAsync(l => l.FileName == fileName);
     }

     private static string ExtractFileType(string fileNameNoExt)
     {
         var match = FileTypeRegex().Match(fileNameNoExt);
         return match.Success ? match.Groups[1].Value : "unknown";
     }

     private static int ExtractLogFileId(string fileNameNoExt)
     {
         var match = FileIdRegex().Match(fileNameNoExt);
         return match.Success ? int.Parse(match.Groups[1].Value) : 0;
     }


     private static LogEntry? ParseLine(string line, int parsedLogId, int lineNum)
     {
         var parts = line.Split("->", StringSplitOptions.TrimEntries);
         if (parts.Length < 2) return null;

         var dateTimePart = parts[0].Trim();
         string ipPart = string.Empty;
         string statusAndRestPart;

         // Check if the IP address is present
         if (parts.Length == 3)
         {
             ipPart = parts[1].Trim();
             statusAndRestPart = parts[2].Trim();
         }
         else
         {
             // Assume the IP address is missing and adjust accordingly
             statusAndRestPart = parts[1].Trim();
         }

         var statusPart = statusAndRestPart.Split(':', StringSplitOptions.TrimEntries)[0];
         var actionDetailsPart = ActionDetailsRegex().Match(statusAndRestPart);

         string action = actionDetailsPart.Groups[1].Value.Trim();
         string details = actionDetailsPart.Groups.Count > 2 ? actionDetailsPart.Groups[2].Value.Trim() : string.Empty;

         return new LogEntry
         {
             ParsedLogId = parsedLogId,
             LineNum = lineNum,
             EntryDate = DateTime.ParseExact(dateTimePart, "ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture),
             IPaddress = ipPart,
             Status = statusPart,
             Action = action,
             Details = details
         };
     }

     // generates all regexes at compile time
     [GeneratedRegex(@"^(.*?)_\d+$")]
     private static partial Regex FileTypeRegex();

     [GeneratedRegex(@"_([0-9]+)$")]
     private static partial Regex FileIdRegex();

     [GeneratedRegex(@"Action=\[(.*?)\](?:, Details=\[(.*?)\])?", RegexOptions.Compiled)]
     private static partial Regex ActionDetailsRegex();
 }

Файл program.cs:

    namespace LogParserApp;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;

internal partial class Program : ProgramBase
{
    public static async Task Main(string[] args)
    {
        var settings = ParseArguments(args);

        if (!settings.TryGetValue("filetype", out List<string>? value) || value.Count == 0)
        {
            Console.WriteLine("Please specify at least one filetype using '-filetype \"smtp, pop3\"'.");
            return;
        }

        var host = CreateHostBuilder(args).Build();

        // Access the configuration and the LogFileProcessor service
        var config = host.Services.GetRequiredService<IConfiguration>();

        string? folderPath = settings.TryGetValue("folderpath", out List<string>? value1) && value1.Count > 0 ? value1[0]
                              : config["LogFileSettings:FolderPath"];

        string? archivePath = settings.TryGetValue("archivepath", out List<string>? value2) && value2.Count > 0 ? value2[0]
                              : config["LogFileSettings:ArchivePath"];

        var logFileProcessor = host.Services.GetRequiredService<LogFileProcessor>();

        string postProcess = settings.TryGetValue("postprocess", out List<string>? value3) && value3.Count > 0 ? value3[0].ToLower() : "keep";


        foreach (var fileType in value)
        {
            var logFiles = Directory.GetFiles(folderPath ?? "C:\\logs", $"{fileType}_*.txt")
                .Select(file => new
                {
                    FileName = file,
                    OrderKey = int.Parse(OrderKeyRegex().Match(Path.GetFileName(file)).Groups[1].Value)
                })
                .OrderBy(f => f.OrderKey)
                .Select(f => f.FileName);

            //long memOffset = GC.GetTotalMemory(forceFullCollection: true); //for tracking memory

            foreach (var file in logFiles)
            {
                // EnsureAvailableMemory(); //to keep program from crashing, no joy

                //long startMem = GC.GetTotalMemory(forceFullCollection: true);  //for tracking memory

                Console.WriteLine($"Processing file: {file}");
                var processSuccess = (await logFileProcessor.ProcessLogFileAsync(file));

                if (processSuccess)
                {
                    switch (postProcess)
                    {
                        case "archive":
                            string targetPath = Path.Combine(archivePath ?? "C:\\logs\\archive", Path.GetFileName(file));
                            File.Move(file, targetPath);
                            Console.WriteLine($"Archived file to: {targetPath}");
                            break;
                        case "delete":
                            File.Delete(file);
                            Console.WriteLine($"Deleted file: {file}");
                            break;
                        case "keep":
                            // Nothing to do, may add something later to keep, but rename, or what-have-you
                            break;
                    }
                }
                else
                {
                    Console.WriteLine($"Processing failed for file: {file}, skipping post-processing steps.");
                }

                GC.Collect(0, GCCollectionMode.Forced);

                //long endMem = GC.GetTotalMemory(forceFullCollection: true);  //for tracking memory
                //Console.WriteLine($"Memory Utilized: {(endMem - startMem) / 1048576M:N2} MB");  //for tracking memory
                //Console.WriteLine($"Running Memory: {(endMem - memOffset) / 1048576M:N2} MB");  //for tracking memory

            }
        }

        await host.RunAsync();
    }

    static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
        })
        .ConfigureServices((hostContext, services) =>
        {
            services.AddDbContext<LogDbContext>(options =>
                options.UseSqlServer(hostContext.Configuration.GetConnectionString("DefaultConnection")));

            services.AddScoped<LogFileProcessor>();
            services.AddLogging(); 

            services.AddSingleton<IConfiguration>(hostContext.Configuration);
        })
        .ConfigureLogging(logging => {
            logging.ClearProviders();
            logging.AddConsole();
            logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning);
        });


    // generates a regex at compile time
    [GeneratedRegex(@"^.*?_(\d+)\.txt$")]
    private static partial Regex OrderKeyRegex();


    // this doesn't help - garbage collection never actually occurs, so it stays at 1GB & tries again indefinitely
    public static void EnsureAvailableMemory()
    {
        const long maxAllowedMemory = 1_073_741_824; // Set threshold to 1 GB

        while (true)
        {
            long memoryUsed = GC.GetTotalMemory(false);
            Console.WriteLine($"Memory used: {memoryUsed} bytes");

            if (memoryUsed < maxAllowedMemory)
            {
                break;
            }

            Console.WriteLine("Memory usage is too high, forcing garbage collection.");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("Garbage collection complete, pausing for a few seconds...");
            Thread.Sleep(5000); // Wait 5 seconds before checking again
        }
    }

}

Обратите внимание на последний метод (я пробовал разные вещи с GC, но безуспешно).

Сущности (вероятно не помогут, но вот они)

public class ParsedLog
{
    public int Id { get; set; }
    public string FileName { get; set; } = string.Empty;
    public string LogType { get; set; } = string.Empty;
    public int LogFileId { get; set; }
    public DateTime DateParsed { get; set; }
    public DateTime FileDate { get; set; }
    public string? FileHash { get; set; }  // SHA-256 hash of the file
}

public class LogEntry
{
    public long Id { get; set; }
    public int ParsedLogId { get; set; }
    public int LineNum { get; set; }
    public DateTime EntryDate { get; set; }
    public string IPaddress { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string Action { get; set; } = string.Empty;
    public string Details { get; set; } = string.Empty;

}


public class LogDbContext(DbContextOptions<LogDbContext> options) : DbContext(options)
{
    public DbSet<LogEntry> LogEntries { get; set; }
    public DbSet<ParsedLog> ParsedLogs { get; set; }
}

Я планирую провести рефакторинг для очевидного повышения скорости (предварительное хеширование файлов, использование интервалов, групповая вставка и т. д.), но проблема с памятью становится довольно серьезной при обработке тысяч файлов.

Вот некоторые результаты, демонстрирующие увеличение объема памяти до 1 ГБ по 1-2 МБ за раз.

    PS D:\Projects\LogParserApp> dotnet run -filetype "smtp" -postprocess "archive"

Processing file: D:\EmailLogs\smtp_0.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_0.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_0.txt
Memory Utilized: 12.27 MB
Running Memory: 12.49 MB
Processing file: D:\EmailLogs\smtp_1.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_1.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_1.txt
Memory Utilized: 2.78 MB
Running Memory: 15.27 MB
Processing file: D:\EmailLogs\smtp_2.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_2.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_2.txt
Memory Utilized: 2.48 MB
Running Memory: 17.74 MB
Processing file: D:\EmailLogs\smtp_3.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_3.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_3.txt
Memory Utilized: 3.28 MB
Running Memory: 21.03 MB
Processing file: D:\EmailLogs\smtp_4.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_4.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_4.txt
Memory Utilized: 2.28 MB
Running Memory: 23.31 MB
Processing file: D:\EmailLogs\smtp_5.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_5.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_5.txt
Memory Utilized: 2.55 MB
Running Memory: 25.86 MB
...
...
...
Processing file: D:\EmailLogs\smtp_370.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_370.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_370.txt
Memory Utilized: 2.36 MB
Running Memory: 999.33 MB
Processing file: D:\EmailLogs\smtp_371.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_371.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_371.txt
Memory Utilized: 2.59 MB
Running Memory: 1,001.92 MB
Processing file: D:\EmailLogs\smtp_372.txt
info: LogParserApp.LogFileProcessor[0]
      Log file: smtp_372.txt processed and data committed to the database.
Archived file to: D:\EmailLogs\ArchivedLogs\smtp_372.txt
Memory Utilized: 2.24 MB
Running Memory: 1,004.16 MB

Это на 373 файла - представьте себе 10 тысяч файлов. :)

Добавление сущностей в DbContext добавит их в кэш отслеживания. Я бы использовал зависимость DbContextFactory, а не DbContext с одной областью действия, чтобы на каждой итерации для вставки журналов и т. д. автоматически создавался и удалялся новый экземпляр DbContext. В качестве альтернативы вы можете отсоединить все объекты после добавления, чтобы удалить их из кеша отслеживания, чтобы можно было соответствующим образом очистить ссылки.

Steve Py 16.05.2024 02:19

Откуда вы знаете, что память «утекает»? Я не думаю, что это утечка. Скорее всего, это задуманная функция Entity Framework для отслеживания сущностей в DbContext.

Vlad DX 16.05.2024 02:41

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

Vlad DX 16.05.2024 02:42

Единственное, что нужно сделать, это удалить флажок file.Exists(). Вместо этого добавьте сообщение журнала и return false как часть обработчика исключений FileNotFoundException. Это в среднем сэкономит дисковый ввод-вывод. Кроме того, измените File.ReadAllLinesAsync() на просто File.ReadLinesAsync()await цикл)

Joel Coehoorn 16.05.2024 03:31

Используйте профилировщик памяти, например ANTS или dotMemory, чтобы узнать, какие объекты создаются. Я рискну предположить, что на самом деле это может быть Regex, генерирующий динамические модули: RegexOptions.Compiled не имеет смысла для GeneratedRegex и может оказаться контрпродуктивным. Но я согласен, вам определенно не следует кэшировать DbContext в долгосрочной перспективе.

Charlieface 16.05.2024 04:16

Я бы сказал, что этот ответ и комментарии к нему могут быть полезны.

Guru Stron 16.05.2024 07:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
289
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Похоже, вы используете один и тот же экземпляр LogFileProcessor (и, соответственно, один и тот же контекст) для каждого файла журнала. Если вы не укажете иное, любые объекты, добавленные в контекст, будут продолжать отслеживаться, поэтому чем больше файлов будет обработано, тем больше объектов будет отслеживаться, что приведет к более высокому использованию памяти.

Вы можете попробовать очистить контекст после успешной обработки каждого файла. Просто вызовите _dbContext.ChangeTracker.Clear() в конце метода ProcessLogFileAsync.

Кроме того, будьте осторожны с прямым вызовом GC.Collect внутри вашего кода, если вы действительно не знаете, что делаете.

Одним из соображений является эта строка:

_lines = await File.ReadAllLinesAsync(filePath);

... который загружает весь файл в память сразу.

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

Для этого сначала удалите объявление private string[] _lines = []; в начале класса и соответствующее _lines = []; в конце finally — они вам не нужны, и да, для более простого кода.

Затем мы перепишем эти три строки:

_lines = await File.ReadAllLinesAsync(filePath);
int lineNum = 0;
foreach (var line in _lines)

вот так:

int lineNum = 0;
await foreach (var line in File.ReadLinesAsync(filePath))

Больше ничего не меняется. Обратите внимание, это просто ReadLinesAsync(), без All.


Пока я здесь, вы также можете сэкономить время ожидания дискового ввода-вывода в среднем один раз для каждого файла, удалив этот код:

if (!File.Exists(filePath))
{
    _logger.LogError("File not found: {filePath}", filePath);
    return result;
}

Действительно: просто удалите его. Пусть код попытается открыть файл независимо от того, существует он или нет.

Вместо этого добавьте этот обработчик исключений над строкой catch (Exception ex), чтобы у вас было два обработчика исключений на одной и той же try:

catch (FileNotFoundException ex)
{
    _logger.LogError("File not found: {filePath}", filePath);
    return false;
}

Если файл не существует и вы пытаетесь его открыть, у вас будет такой же объем операций ввода-вывода на диске, как если бы вы выполнили проверку .Exists(): в любом случае это один обход файловой системы. Вы теряете затраты на раскручивание стека для создания объекта обработчика исключений, поэтому в общем случае мы избегаем использования подобных исключений для управления потоком. Но если файл существует, вы полностью сохранили проверку .Exists()... потенциально «5000 файлов журналов (или более)». Учитывая, что дисковый ввод-вывод — это самое медленное, что вы можете сделать на компьютере (намного медленнее, даже чем раскручивание стека вызовов для обработчика исключений!), это может суммироваться, и пропустить ручную проверку Exists() в пользу обработчика исключений почти невозможно. всегда правильный вариант, если вы все равно хотите открыть файл.

Этот трюк особенно полезен для вашего кода, поскольку проверка .Exists() не была асинхронной. И поскольку это, скорее всего, связано с вводом-выводом, а не с ЦП, перенесение некоторой потенциальной нагрузки с ввода-вывода на ЦП/Память для очистки стека также может быть большим выигрышем для создания пространств, где асинхронный код может фактически переключаться на задачи. заняться другой работой.

Кроме того, поскольку файловая система нестабильна, у вас в любом случае должен быть обработчик исключений — одной проверки .Exists() недостаточно, и поэтому ее удаление, чтобы полагаться только на исключение, фактически экономит код в целом. Фактически, вам также следует иметь обработчики для DirectoryNotFoundException, SecurityException, IOException, UnauthorizedAccessException и всех других вещей из раздела «Исключения» документации, которые могут привести к тому, что файл не будет читаться, кроме того, существует он или нет, но упадет. вернуться к базовому типу Exception достаточно для большинства ситуаций.


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

Представление высокого уровня выглядит примерно так, где ProcessOneFile() может даже быть синхронным:

var tasks = new List<Task>();
foreach(var filePath in files)
{
    // Do NOT await here
    tasks.Add(Task.Run( f => ProcessOneFile(filePath) );
}
// await them all at once here instead
await Task.WhenAll(tasks);

Учитывая количество имеющихся у вас файлов, я мог бы дополнительно объединить их в группы, скажем, по 50, но это общая идея.

Но даже в этом случае я чувствую, что этот код, скорее всего, будет привязан к вводу-выводу, а не к ЦП, так что вся асинхронная работа может ухудшать ситуацию, а не улучшать ее... поэтому вам может быть лучше убедиться, что вы не вызывая поломки диска.

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

MC9000 16.05.2024 19:06

@ MC9000 MC9000 Он может держать его открытым, но только до тех пор, пока вы не закончите работу с файлом, а ввод-вывод для фактического чтения файла занимает большую часть времени... часть обработки обычно происходит почти мгновенно. И поскольку он предназначен только для чтения, он не обязательно должен быть эксклюзивной блокировкой. Есть исключения, но, как правило, вы будете НАМНОГО лучше работать построчно.

Joel Coehoorn 16.05.2024 19:12

Еще раз спасибо — мне нравится ваше последнее предложение (я сделал нечто подобное несколько лет назад, одновременно считывая цены на криптовалюту на нескольких биржах, прежде чем C# сделал это намного проще). Я удалил все, что связано с файловой системой, из блока транзакций (отдельный блок try-catch для каждого, но не вложенный)

MC9000 17.05.2024 05:32
Ответ принят как подходящий

Самый простой способ справиться с этим — создать область обслуживания для каждого файла;

foreach (var file in logFiles)
{
    using var scope = host.Services.CreateScope();
    var logFileProcessor = scope.ServiceProvider.GetRequiredService<LogFileProcessor>();
...

Все ваши службы с областью действия будут удалены при удалении scope.

Это сразу сработало. Теперь, когда я больше думаю об этом, это имеет смысл (не знал, что вы можете это сделать) - я работал с изменениями - использование памяти теперь не изменилось.

MC9000 16.05.2024 19:00

Другие вопросы по теме