Как узнать, когда все мои потоки закончили выполняться в рекурсивном методе?
Я работал над проектом по созданию веб-страниц.
У меня две проблемы, одна из которых представляет количество URL-адресов, обработанных в процентах, но гораздо большая проблема заключается в том, что я не могу понять, как я знаю, когда все потоки, которые я создаю, полностью завершены.
ПРИМЕЧАНИЕ: я знаю, что параллельный foreach после того, как все сделано, перемещается на НО, это внутри рекурсивного метода.
Мой код ниже:
public async Task Scrape(string url)
{
var page = string.Empty;
try
{
page = await _service.Get(url);
if (page != string.Empty)
{
if (regex.IsMatch(page))
{
Parallel.For(0, regex.Matches(page).Count,
index =>
{
try
{
if (regex.Matches(page)[index].Groups[1].Value.StartsWith("/"))
{
var match = regex.Matches(page)[index].Groups[1].Value.ToLower();
if (!links.Contains(BaseUrl + match) && !Visitedlinks.Contains(BaseUrl + match))
{
Uri ValidUri = WebPageValidator.GetUrl(match);
if (ValidUri != null && HostUrls.Contains(ValidUri.Host))
links.Enqueue(match.Replace(".html", ""));
else
links.Enqueue(BaseUrl + match.Replace(".html", ""));
}
}
}
catch (Exception e)
{
log.Error("Error occured: " + e.Message);
Console.WriteLine("Error occured, check log for further details."); ;
}
});
WebPageInternalHandler.SavePage(page, url);
var context = CustomSynchronizationContext.GetSynchronizationContext();
Parallel.ForEach(links, new ParallelOptions { MaxDegreeOfParallelism = 25 },
webpage =>
{
try
{
if (WebPageValidator.ValidUrl(webpage))
{
string linkToProcess = webpage;
if (links.TryDequeue(out linkToProcess) && !Visitedlinks.Contains(linkToProcess))
{
ShowPercentProgress();
Thread.Sleep(15);
Visitedlinks.Enqueue(linkToProcess);
Task d = Scrape(linkToProcess);
Console.Clear();
}
}
}
catch (Exception e)
{
log.Error("Error occured: " + e.Message);
Console.WriteLine("Error occured, check log for further details.");
}
});
Console.WriteLine("parallel finished");
}
}
catch (Exception e)
{
log.Error("Error occured: " + e.Message);
Console.WriteLine("Error occured, check log for further details.");
}
}
Обратите внимание, что Scrape
вызывается несколько раз (рекурсивно)
вызовите метод так:
public Task ExecuteScrape()
{
var context = CustomSynchronizationContext.GetSynchronizationContext();
Scrape(BaseUrl).ContinueWith(x => {
Visitedlinks.Enqueue(BaseUrl);
}, context).Wait();
return null;
}
который в свою очередь называется так:
static void Main(string[] args)
{
RunScrapper();
Console.ReadLine();
}
public static void RunScrapper()
{
try
{
_scrapper.ExecuteScrape();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
мой результат:
Как мне это решить?
2 ответа
(Этично ли для меня отвечать на вопрос об очистке веб-страницы?)
Не звони Scrape
рекурсивно. Поместите список URL, которые вы хотите очистить в ConcurrentQueue
и начать обработку этой очереди. Поскольку процесс очистки страницы возвращает больше URL, просто добавьте их в одну и ту же очередь.
Я бы тоже не использовал просто строку. Я рекомендую создать класс как
public class UrlToScrape //because naming things is hard
{
public string Url { get; set; }
public int Depth { get; set; }
}
Независимо от того, как вы выполняете это, это рекурсивно, поэтому вы должны каким-то образом отслеживать, насколько глубоко вы находитесь. Веб-сайт может намеренно генерировать URL-адреса, которые отправят вас в бесконечную рекурсию. (Если они сделали это, то они не хотят, чтобы вы чистили их сайт. Кто-нибудь хочет, чтобы люди чистили их сайт?)
Когда ваша очередь пуста, это не значит, что вы закончили. Очередь может быть пустой, но процесс очистки последнего удаленного URL-адреса может все еще добавить дополнительные элементы в эту очередь, поэтому вам нужен способ для учета этого.
Вы можете использовать потокобезопасный счетчик (int
с помощью Interlocked.Increment/Decrement
) что вы увеличиваете, когда начинаете обрабатывать URL, и уменьшаете, когда заканчиваете. Вы закончили, когда очередь пуста, а количество внутрипроцессных URL равно нулю.
Это очень грубая модель для иллюстрации концепции, а не то, что я бы назвал утонченным решением. Например, вам все еще нужно учитывать обработку исключений, и я понятия не имею, куда идут результаты и т. Д.
public class UrlScraper
{
private readonly ConcurrentQueue<UrlToScrape> _queue = new ConcurrentQueue<UrlToScrape>();
private int _inProcessUrlCounter;
private readonly List<string> _processedUrls = new List<string>();
public UrlScraper(IEnumerable<string> urls)
{
foreach (var url in urls)
{
_queue.Enqueue(new UrlToScrape {Url = url, Depth = 1});
}
}
public void ScrapeUrls()
{
while (_queue.TryDequeue(out var dequeuedUrl) || _inProcessUrlCounter > 0)
{
if (dequeuedUrl != null)
{
// Make sure you don't go more levels deep than you want to.
if (dequeuedUrl.Depth > 5) continue;
if (_processedUrls.Contains(dequeuedUrl.Url)) continue;
_processedUrls.Add(dequeuedUrl.Url);
Interlocked.Increment(ref _inProcessUrlCounter);
var url = dequeuedUrl;
Task.Run(() => ProcessUrl(url));
}
}
}
private void ProcessUrl(UrlToScrape url)
{
try
{
// As the process discovers more urls to scrape,
// pretend that this is one of those new urls.
var someNewUrl = "http://discovered";
_queue.Enqueue(new UrlToScrape { Url = someNewUrl, Depth = url.Depth + 1 });
}
catch (Exception ex)
{
// whatever you want to do with this
}
finally
{
Interlocked.Decrement(ref _inProcessUrlCounter);
}
}
}
Если бы я делал это по-настоящему ProcessUrl
Метод будет своим собственным классом, и он будет принимать HTML, а не URL. В таком виде сложно провести юнит тест. Если бы он был в отдельном классе, вы могли бы передать HTML, проверить, что он выводит результаты куда-то, и что он вызывает метод для постановки в очередь новых URL-адресов, которые он находит.
Также неплохо поддерживать очередь в виде таблицы базы данных. В противном случае, если вы обрабатываете кучу URL-адресов и вам нужно остановиться, вам придется начинать все сначала.
Не можете ли вы добавить все задачи Task d
к некоторому типу параллельной коллекции вы проходите через все рекурсивные вызовы (через аргумент метода), а затем просто вызываете Task.WhenAll(tasks).Wait()
?
Вам нужен промежуточный метод (делает его чище), который запускает базу Scrape
Звонок и проходит в пустую коллекцию задач. Когда базовый вызов возвращается, у вас есть все задачи и вы просто ждете их.
public async Task Scrape (
string url) {
var tasks = new ConcurrentQueue<Task>();
//call your implementation but
//change it so that you add
//all launched tasks d to tasks
Srape(url, tasks);
//1st option: Wait().
//This will block caller
//until all tasks finish
Task.WhenAll(tasks).Wait();
//or 2nd option: await
//this won't block and will return to caller.
//Once all tasks are finished method
//will resume in WriteLine
await Task.WhenAll(tasks);
Console.WriteLine("Finished!"); }
Простое правило: если вы хотите знать, когда что-то заканчивается, первым делом нужно отследить это. В своей текущей реализации вы, по сути, запускаете и забываете все запущенные задачи...