在C#中线程化未知数量的线程

时间:2009-04-30 16:55:43

标签: c# asp.net multithreading

我目前正在编写一个站点地图生成器,用于抓取站点以获取URL并构建xml站点地图。由于大多数等待都花在对uri的请求上,我正在使用线程,特别是在ThreadPool对象中的构建。

为了让主线程等待未知数量的线程完成,我已经实现了以下设置。我觉得这不是一个好的解决方案,任何线程专家都可以告诉我这个解决方案有什么问题,或者建议一个更好的方法来实现它吗?

EventWaitHandle设置为EventResetMode.ManualReset

这是线程方法

protected void CrawlUri(object o)
    {

        try
        {
            Interlocked.Increment(ref _threadCount);
            Uri uri = (Uri)o;

            foreach (Match match in _regex.Matches(GetWebResponse(uri)))
            {
                Uri newUri = new Uri(uri, match.Value);

                if (!_uriCollection.Contains(newUri))
                {
                    _uriCollection.Add(newUri);
                    ThreadPool.QueueUserWorkItem(_waitCallback, newUri);
                }
            }
        }
        catch
        {
            // Handle exceptions
        }
        finally
        {
            Interlocked.Decrement(ref _threadCount);
        }

        // If there are no more threads running then signal the waithandle
        if (_threadCount == 0)
            _eventWaitHandle.Set();
    }

这是主线程方法

// Request first page (based on  host)
Uri root = new Uri(context.Request.Url.GetLeftPart(UriPartial.Authority));

// Begin threaded crawling of the Uri
ThreadPool.QueueUserWorkItem(_waitCallback, root);
Thread.Sleep(5000); // TEMP SOLUTION: Sleep for 5 seconds
_eventWaitHandle.WaitOne();

 // Server the Xml Sitemap
 context.Response.ContentType = "text/xml";
 context.Response.Write(GetXml().OuterXml);

非常感谢任何想法:)

3 个答案:

答案 0 :(得分:1)

好吧,首先你可以创建一个开始取消设置的ManualResetEvent,这样你就不用在等待之前就睡觉了。其次,您需要在Uri集合周围进行线程同步。你可以得到一个竞争条件,其中一个两个线程通过“这个Uri尚不存在”检查并且他们添加重复项。另一个竞争条件是两个线程可以通过if (_threadCount == 0)检查,它们都可以设置事件。

最后,通过使用异步BeginGetRequest,您可以提高整体效率。您的解决方案现在保持一个线程等待每个请求。如果你使用异步方法和回调,你的程序将使用更少的内存(每个线程1MB),并且不需要几乎同样的线程上下文切换。

这是一个应该说明我在说什么的例子。出于好奇,我测试了它(有一个深度限制),它确实有效。

public class CrawlUriTool
{
    private Regex regex;
    private int pendingRequests;
    private List<Uri> uriCollection;
    private object uriCollectionSync = new object();
    private ManualResetEvent crawlCompletedEvent;

    public List<Uri> CrawlUri(Uri uri)
    {
        this.pendingRequests = 0;
        this.uriCollection = new List<Uri>();
        this.crawlCompletedEvent = new ManualResetEvent(false);
        this.StartUriCrawl(uri);
        this.crawlCompletedEvent.WaitOne();

        return this.uriCollection;
    }

    private void StartUriCrawl(Uri uri)
    {
        Interlocked.Increment(ref this.pendingRequests);

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

        request.BeginGetResponse(this.UriCrawlCallback, request);
    }

    private void UriCrawlCallback(IAsyncResult asyncResult)
    {
        HttpWebRequest request = asyncResult.AsyncState as HttpWebRequest;

        try
        {
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);

            string responseText = this.GetTextFromResponse(response); // not included

            foreach (Match match in this.regex.Matches(responseText))
            {
                Uri newUri = new Uri(response.ResponseUri, match.Value);

                lock (this.uriCollectionSync)
                {
                    if (!this.uriCollection.Contains(newUri))
                    {
                        this.uriCollection.Add(newUri);
                        this.StartUriCrawl(newUri);
                    }
                }
            }
        }
        catch (WebException exception)
        {
            // handle exception
        }
        finally
        {
            if (Interlocked.Decrement(ref this.pendingRequests) == 0)
            {
                this.crawlCompletedEvent.Set();
            }
        }
    }
}

答案 1 :(得分:0)

在执行这种逻辑时,我通常会尝试创建一个表示每个异步任务的对象以及它需要运行的数据。我通常会将此对象添加到要完成的任务集合中。线程池将这些任务调整好,当任务完成时,我会让对象本身从“待完成”集合中删除,可能是集合本身的信号。

所以当“待完成”集合为空时你就完成了;主线程可能会被完成的每个任务唤醒一次。

答案 2 :(得分:0)

你可以查看Task Parallel Library的CTP,它可以让你更简单。您正在做的事情可以分为“任务”,块或工作单元,如果您提供任务,TPL可以为您并行化。它内部也使用一个线程池,但它更容易使用,并提供了许多选项,如等待所有任务完成。查看this Channel9视频,其中解释了各种可能性,以及显示并行递归遍历树的演示,这似乎非常适用于您的问题。

但是,它仍然是预览版,直到.NET 4.0才会发布,所以它没有保修,你必须手动将提供的System.Threading.dll(在安装文件夹中找到)包含到你的项目,我不知道这是否是你的选择。