Когда я запускаю этот код консольного приложения C# (с использованием проекта с открытым исходным кодом HtmlAgilityPack), использование памяти со временем начинает накапливаться, что выглядит как утечка памяти, но я не могу отследить ее, несмотря на принудительную сборку мусора, повторное использование объекты, присваивая значениям null и очищая информацию.
Где здесь ошибка в моем коде? Или это проблема с HtmlAgilityPack?
После 10 минут работы этого цикла все просто останавливается. И это на системе со 128 ГБ ОЗУ.
using HtmlAgilityPack;
internal class Program
{
private static int setNum = 16020;
static HtmlDocument doc = null;
static HtmlWeb web = new HtmlWeb();
static void Main(string[] args)
{
ScrapeTypeYearName(setNum);
Console.WriteLine("Press Enter to exit...");
Console.ReadLine(); // Wait for the user to press Enter
}
public static void ScrapeTypeYearName(int setNum)
{
string setUrl = "https://www.tcdb.com/Checklist.cfm/sid/" + setNum;
doc = web.Load(setUrl);
// Process breadcrumb information
string category = "";
string year = "";
string company = "";
string setName = "";
var breadcrumb = doc.DocumentNode.SelectSingleNode("//div[@class='d-none d-md-block']/nav[@aria-label='breadcrumb']");
if (breadcrumb != null)
{
var breadcrumbItems = breadcrumb.SelectNodes("./ol/li/a");
if (breadcrumbItems != null && breadcrumbItems.Count >= 4)
{
category = breadcrumbItems[1].InnerText.Trim();
year = breadcrumbItems[3].InnerText.Trim();
Console.WriteLine("Category: " + category);
Console.WriteLine("Year: " + year);
var companyNode = breadcrumb.SelectSingleNode("following::h1[@class='site']");
var setNode = breadcrumb.SelectSingleNode("following::h3[@class='site']");
if (companyNode != null)
{
company = companyNode.InnerText.Trim();
company = company.Replace(year, "").Trim();
Console.WriteLine("Company: " + company);
}
if (setNode != null)
{
setName = setNode.InnerText.Trim();
Console.WriteLine("Set: " + setName);
}
}
}
// Do stuff here with the
// Clear the main document to free up memory
doc?.DocumentNode.RemoveAll();
setNum += 1;
if (setNum < 99999)
{
Console.WriteLine($"");
Console.WriteLine($"On set #: {setNum}");
// Force garbage collection before the next recursive call
GC.Collect();
GC.WaitForPendingFinalizers();
ScrapeTypeYearName(setNum);
}
}
}
Да, он увеличивает setNum
на единицу и просто перезапускает функцию в конце. Является ли это причиной рекурсивности внутри функции? Я не знаю, почему GC все равно не очищает память.
GC
может очистить только управляемую память, на которую больше нет ссылок. Вы уверены, что doc?.DocumentNode.RemoveAll()
действительно удаляет ссылки?
Проблема в том, что вы вызываете ScrapeTypeYearName()
рекурсивно. Каждый вызов создает новый стек с новыми локальными переменными метода, который вы вызываете. Это также означает, что все «предыдущие» стеки методов «приостанавливаются», но не удаляются и не «восстанавливаются» сборщиком мусора. У вас есть переменные setUrl
, category
, year
, company
, setName
и breadcrumb
, которые ссылаются на объекты в куче. И у вас есть эти ссылки в этих переменных для каждого стека методов. А поскольку у вас есть стек вызовов методов, состоящий из 99999-16020=83979 стеков методов, и шесть переменных, содержащих потенциально разные объекты в куче, в конечном итоге у вас закончилось место в куче.
Нет необходимости вызывать этот метод ScrapeTypeYearName()
рекурсивно. Удалите рекурсивный вызов. Простой цикл for
вне этого метода вполне подойдет. Код может быть таким простым, как:
for (int setNum=16020; setNum<99999; setNum++)
{
ScrapeTypeYearName(setNum);
}
Хорошо, спасибо, что объяснили! Я обязательно добавлю обратный вызов (или какой-нибудь обработчик завершения), прежде чем продолжить цикл, потому что я не хочу, чтобы 99999 веб-вызовов происходили одновременно. Я протестирую еще раз и посмотрю, что произойдет.
Переместил цикл наружу и добавил async/await, и это исправило ситуацию. Большое спасибо за помощь.
У вас есть рекурсивный вызов метода
ScrapeTypeYearName()
, это намеренно?