Как узнать, когда все мои потоки закончили выполняться в рекурсивном методе?

Я работал над проектом по созданию веб-страниц.

У меня две проблемы, одна из которых представляет количество 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!"); }

Простое правило: если вы хотите знать, когда что-то заканчивается, первым делом нужно отследить это. В своей текущей реализации вы, по сути, запускаете и забываете все запущенные задачи...

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