Task.WaitAll没有等待其他异步方法

3
我正在使用Microsoft.Bcl库(该库没有Task.WhenAll)的便携式类库异步检索一些rss文章。每篇文章都有一个指向rss评论的网址,我也需要异步地获取它们。
下面的代码是我的库。我调用GetArticles(),但它不返回任何内容,它创建了一个任务列表,调用GetComments()以异步获取评论。
我尝试在GetArticles中使用Task.WaitAll等待评论,但它不会阻塞线程。如果您能提供帮助,将不胜感激。
    private const string ArticlesUri = "";

    public async Task<List<ArticleBrief>> GetArticles()
    {
        var results = new List<ArticleBrief>();
        try
        {
            var wfw = XNamespace.Get("http://wellformedweb.org/CommentAPI/");
            var media = XNamespace.Get("http://search.yahoo.com/mrss/");
            var dc = XNamespace.Get("http://purl.org/dc/elements/1.1/");

            var t = await WebHttpRequestAsync(ArticlesUri);
            StringReader stringReader = new StringReader(t);
            using (var xmlReader = System.Xml.XmlReader.Create(stringReader))
            {
                var doc = System.Xml.Linq.XDocument.Load(xmlReader);
                results = (from e in doc.Element("rss").Element("channel").Elements("item")
                           select
                               new ArticleBrief()
                               {
                                   Title = e.Element("title").Value,
                                   Description = e.Element("description").Value,
                                   Published = Convert.ToDateTime(e.Element("pubDate").Value),
                                   Url = e.Element("link").Value,
                                   CommentUri = e.Element(wfw + "commentRss").Value,
                                   ThumbnailUri = e.Element(media + "thumbnail").FirstAttribute.Value,
                                   Categories = GetCategoryElements(e.Elements("category")),
                                   Creator = e.Element(dc + "creator").Value
                               }).ToList();

            }

            var tasks = new Queue<Task>();
            foreach (var result in results)
            {
                tasks.Enqueue(
                    Task.Factory.StartNew(async ()=>
                        {
                            result.Comments = await GetComments(result.CommentUri);
                        }
                    ));
            }

            Task.WaitAll(tasks.ToArray());
        }
        catch (Exception ex)
        {
            // should do some other
            // logging here. for now pass off
            // exception to callback on UI
            throw ex;
        }

        return results;
    }

    public async Task<List<Comment>> GetComments(string uri)
    {
        var results = new List<Comment>();
        try
        {
            var wfw = XNamespace.Get("http://wellformedweb.org/CommentAPI/");
            var media = XNamespace.Get("http://search.yahoo.com/mrss/");
            var dc = XNamespace.Get("http://purl.org/dc/elements/1.1/");

            var t = await WebHttpRequestAsync(uri);
            StringReader stringReader = new StringReader(t);
            using (var xmlReader = System.Xml.XmlReader.Create(stringReader))
            {
                var doc = System.Xml.Linq.XDocument.Load(xmlReader);
                results = (from e in doc.Element("rss").Element("channel").Elements("item")
                           select
                               new Comment()
                               {
                                   Description = e.Element("description").Value,
                                   Published = Convert.ToDateTime(e.Element("pubDate").Value),
                                   Url = e.Element("link").Value,
                                   Creator = e.Element(dc + "creator").Value
                               }).ToList();
            }
        }
        catch (Exception ex)
        {
            // should do some other
            // logging here. for now pass off
            // exception to callback on UI
            throw ex;
        }

        return results;
    }

    private static async Task<string> WebHttpRequestAsync(string url)
    {
        //TODO: look into getting 
        var request = WebRequest.Create(url);
        request.Method = "GET";

        var response = await request.GetResponseAsync();
        return ReadStreamFromResponse(response);
    }

    private static string ReadStreamFromResponse(WebResponse response)
    {
        using (Stream responseStream = response.GetResponseStream())
        using (StreamReader sr = new StreamReader(responseStream))
        {
            string strContent = sr.ReadToEnd();
            return strContent;
        }
    }

    private List<string> GetCategoryElements(IEnumerable<XElement> categories)
    {
        var listOfCategories = new List<string>();

        foreach (var category in categories)
        {
            listOfCategories.Add(category.Value);
        }

        return listOfCategories;
    }

更新解决方案的代码,只需在Enqueue方法上添加.UnWrap():

            var tasks = new Queue<Task>();
            foreach (var result in results)
            {
                tasks.Enqueue(
                    Task.Factory.StartNew(async ()=>
                        {
                            result.Comments = await GetComments(result.CommentUri);
                        }
                    ).Unwrap());
            }

            Task.WaitAll(tasks.ToArray());

4
如果你看到 Task.Factory.StartNew(async ()=>,那就应该警惕了。这意味着你会得到一个 Task<Task>> 的结果。外部的任务只是在启动内部的任务,因此当外部任务完成时,内部任务才刚开始。在这里,你真正应该使用的是 Task.Run,因为它会自动将其解包成一个 Task。如果你不能这样做,那么你需要显式地使用 Unwrap 方法来解包它。 - Servy
5
Stephen Toub撰写了一篇博客文章,介绍了Task.RunTask.Factory.StartNew之间的区别,以及为什么在处理异步任务时应该优先选择Task.Run。请注意,翻译过程中不会添加解释或其他信息。 - Stephen Cleary
谢谢您的评论,那篇文章非常好。 - Bryan
有没有关于此的完整源代码的最终解决方案? - Kiquenet
2个回答

4
它已经适当地等待了。问题在于你创建了一个任务,它又创建了另一个任务(即StartNew返回一个Task<Task>,而你只等待外部Task完成得很快(内部Task完成之前就结束了))。
问题如下:
  • 你真的需要那个内部任务吗?
    • 如果是,那么可以使用Task.Unwrap来获取代表内部和外部任务完成的代理任务,并使用该任务进行等待。
    • 如果不,则可以删除StartNew中async/await的使用,从而没有内部任务(我认为这可能更好,因为不清楚为什么需要内部任务)。
  • 您是否真的需要对异步任务进行同步等待?请阅读Stephen Cleary的一些博客:http://blog.stephencleary.com/2012/02/async-unit-tests-part-1-wrong-way.html
另外,如果您不使用C# 5,请注意关闭foreach变量result。看:

你真的需要在异步任务上执行同步等待吗?如果不需要,那么在不能使用"WhenAll"的情况下,他应该如何进行更改呢?提示:您可以使用"ContinueWhenAll"编写自己的"WhenAll"方法。 - Servy

2
Microsoft.Bcl.Async中,我们不能向Task添加任何静态方法。但是,您可以在TaskEx上找到大多数方法,例如,TaskEx.WhenAll()确实存在。

谢谢!这就是我在寻找的东西,WP7的WhenAll()。 :) - Cœur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接