在递归方法中如何知道我的所有线程何时完成执行?

时间:2018-01-16 17:00:51

标签: c# recursion console-application parallel.foreach parallel.for

我一直致力于网络搜索项目。

我有两个问题,一个是以百分比形式处理的网址数量,但是一个更大的问题是我无法弄清楚我所知道的所有线程何时完全完成。

注意:我知道一个并行的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;
        }
    }

我的结果:

enter image description here

我该如何解决这个问题?

2 个答案:

答案 0 :(得分:1)

(回答有关网页抓取的问题,我是否合乎道德?)

不要递归地拨打Scrape。将要抓取的网址列表放在ConcurrentQueue中,然后开始处理该队列。由于抓取页面的过程会返回更多网址,只需将它们添加到同一个队列中即可。

我也不会只使用字符串。我建议创建一个类

public class UrlToScrape //because naming things is hard
{        
    public string Url { get; set; }
    public int Depth { get; set; }
}

无论你如何执行它都是递归的,所以你必须以某种方式跟踪你的深度。一个网站可以故意生成URL,使您进入无限递归。 (如果他们这样做,那么他们就不希望你刮他们的网站。有人想要人们刮他们的网站吗?)

当你的队列为空时,并不意味着你已经完成了。队列可能是空的,但是抓取最后一个队列的过程仍然可以将更多的项目添加回该队列,因此您需要一种方法来解决这个问题。

您可以使用在开始处理URL时递增的线程安全计数器(int使用Interlocked.Increment/Decrement),并在完成时递减。当队列为空时,您已完成进程内网址的计数为零。

这是一个非常粗略的模型来说明这个概念,而不是我称之为精致的解决方案。例如,您仍然需要考虑异常处理,我不知道结果在哪里等等。

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。

将队列维护为数据库表也不错。否则,如果您正在处理一堆网址并且必须停止,那么您将重新开始。

答案 1 :(得分:0)

你不能将所有任务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!"); }

简单规则:如果您想知道某些事情何时结束,第一步是跟踪。在您当前的实现中,您实际上是在解雇并忘记所有已启动的任务......