none
如何使 Parallel、Task、Task.WaitAll()搭配使用不使 UI 阻塞 RRS feed

  • 问题

  • 先描述一下背景:

    模拟两层结构,UI 层和 Fun 功能层,Fun 里有两个方法,分别是

            private async Task<long> GetTimeOut(Uri address)
            {
                // 模拟访问网络地址获取到获得响应的时间
                await Task.Delay(3000);
                return 0;
            }

            public async Task<long[]> TestSpeed(Uri[] uriArray)
            {
                // 调用 GetTimeOut 来测算每一个地址获得响应的时间
                // 要求并行(同时)执行减少等待
    
                List<Task> taskList = new List<Task>(uriArray.Length);
                foreach (Uri address in uriArray)
                {
                    var task = …………;
                    taskList.Add(task);
                }
    
                // 并行执行所有 Task 任务
                Parallel.ForEach(taskList,() =>{});
    
                // 等待所有任务都完成,才返回
                Task.WaitAll(taskList.ToArray());
    
                return ;
            }

    描述需求:

    UI 层创建 Fun 并调用 Fun.TestSpeed(Uri[]),我想实现每一个 Uri[] 元素都是异步执行,并且并行执行,但所有任务都完成后,Fun.TestSpeed() 才返回一个 Long[] 结果,在整个过程中 UI 不能阻塞,甚至还能完成一个就收到一个的完成“信号”。

    我的问题:

    很明显,在 TestSpeed 方法中我写的都是伪代码或带有错误的,因为尝试写了几次都不对,比较困惑与Parallel和Task的组合使用,并且 Lambda语法也有错误,来论坛求助大神们,希望能帮我写出正确的代码来学习。

    非常期待大神们的回复,多谢多谢!!


    不重要的其实最重要


    2014年4月21日 8:43

答案

  • 感谢 lapheal 的回复

    我用你的方法实际运行了一下,从语法上没有错误,但有两个问题:

    1. 这两种写法所产生的 Task 对象在初始化后,都有 Status 属性为“WaitingForActivation”的。第一种写法里边,有运行完成为“RanToCompletion”的,也有“WaitingForActivation”的,所以导致一直 Wait()在那里,而第二种写法,全部都是“WaitingForActivation”,似乎都没有运行

    2. 两种方法都会因为 Wait 而阻塞 UI,这是因为调用 TestSpeed 方法的线程是 UI 线程吗?如果是这样,岂不是异步也无法解决 Wait 引起的阻塞问题,难道需要单独开一个 Thread 来运行?

    谢谢


    不重要的其实最重要

    不应该在UI上直接调用TestSpeed函数,否则会导致结果不正确,而应该用一个新的Task来调用,比如,假设你要在button1的点击事件里面调用,则:

    private async void button1_Click(object sender, EventArgs e)
    {
        Uri[] uris = new Uri[]
        {
            new Uri("http://www.baidu.com"),
            new Uri("http://www.sina.com.cn"),
            new Uri("http://www.google.com"),
        };
    
        await Task.Run(() => TestSpeed(uris));
    }

    如果你需要获取TestSpeed的结果并显示在UI上,可以把TestSpeed修改如下:

    public long[] TestSpeed(Uri[] uriArray)
    {
        long[] results = new long[uriArray.Length];
    
        Parallel.For(0, uriArray.Length, i =>
        {
            Task<long> task = GetTimeOut(uriArray[i]);
            task.Wait();
            results[i] = task.Result;
        });
    
        return results;
    }

    然后在调用时(假设显示在label1上):

    private async void button1_Click(object sender, EventArgs e)
    {
        Uri[] uris = new Uri[]
        {
            new Uri("http://www.baidu.com"),
            new Uri("http://www.sina.com.cn"),
            new Uri("http://www.google.com"),
        };
    
        long[] result = await Task<long[]>.Run(() => TestSpeed(uris));
        label1.Text = String.Join(",", result);
    }
    当然,这只是示例代码而已,根据你的需求修改即可。

    2014年4月22日 10:04

全部回复

  • 下面贴出我写的不对的代码,烦请指正

            private async Task<long> GetTimeOut(Uri address)
            {
                System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
    
                using (HttpClient client = new HttpClient())
                {
                    client.Timeout = new TimeSpan(0, 0, 20);
    
                    try
                    {
                        watcher.Start();
                        using (HttpResponseMessage response = await client.GetAsync(address))
                        {
                            watcher.Stop();
                            response.EnsureSuccessStatusCode();
                        }
    
                        return watcher.ElapsedMilliseconds;
                    }
                    catch { return 0; }
                }
            }
    
    
            public async Task TestSpeed(Uri[] uriArray)
            {
                // 根据 Uri 数组创建 Task 数组
                List<Task<Task<long>>> taskList = new List<Task<Task<long>>>(uriArray.Length);
                foreach (Uri u in uriArray)
                {
                    Func<Uri, Task<long>> fun = new Func<Uri, Task<long>>(this.测速);
                    var task = new Task<Task<long>>(fun, TaskCreationOptions.LongRunning);
                    taskList.Add(task);
                }
    
                // 并行运行所有任务
                System.Threading.Tasks.Parallel.For(0, taskList.Count - 1, (i) => taskList[i].Start());
    
                // 遍历所有任务,当有一个任务没完成,就认为没完成,继续等待
                // 必须所有任务都完成才视为运行完毕
                bool allDone = false;
                do
                {
                    foreach (Task t in taskList)
                    {
                        allDone = (t.Status == TaskStatus.RanToCompletion);
                        if (!allDone) { break; }
                    }
                } while (!allDone);
    
                // 全部运行完了
            }

    主要就是 TestSpeed 里边的代码不会写,逻辑越写越乱……


    不重要的其实最重要


    2014年4月21日 8:48
  • 下面贴出我写的不对的代码,烦请指正

            private async Task<long> GetTimeOut(Uri address)
            {
                System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
    
                using (HttpClient client = new HttpClient())
                {
                    client.Timeout = new TimeSpan(0, 0, 20);
    
                    try
                    {
                        watcher.Start();
                        using (HttpResponseMessage response = await client.GetAsync(address))
                        {
                            watcher.Stop();
                            response.EnsureSuccessStatusCode();
                        }
    
                        return watcher.ElapsedMilliseconds;
                    }
                    catch { return 0; }
                }
            }
    
    
            public async Task TestSpeed(Uri[] uriArray)
            {
                // 根据 Uri 数组创建 Task 数组
                List<Task<Task<long>>> taskList = new List<Task<Task<long>>>(uriArray.Length);
                foreach (Uri u in uriArray)
                {
                    Func<Uri, Task<long>> fun = new Func<Uri, Task<long>>(this.测速);
                    var task = new Task<Task<long>>(fun, TaskCreationOptions.LongRunning);
                    taskList.Add(task);
                }
    
                // 并行运行所有任务
                System.Threading.Tasks.Parallel.For(0, taskList.Count - 1, (i) => taskList[i].Start());
    
                // 遍历所有任务,当有一个任务没完成,就认为没完成,继续等待
                // 必须所有任务都完成才视为运行完毕
                bool allDone = false;
                do
                {
                    foreach (Task t in taskList)
                    {
                        allDone = (t.Status == TaskStatus.RanToCompletion);
                        if (!allDone) { break; }
                    }
                } while (!allDone);
    
                // 全部运行完了
            }

    主要就是 TestSpeed 里边的代码不会写,逻辑越写越乱……


    不重要的其实最重要


    TestSpeed可以是这样:

            public void TestSpeed(Uri[] uriArray)
            {
                Parallel.For(0, uriArray.Length, i =>
                {
                    Task<long> task = GetTimeOut(uriArray[i]);
                    task.Wait();
                    Console.WriteLine(uriArray[i].ToString() + ": " + task.Result);
                });
            }

    或者

         public void TestSpeed(Uri[] uriArray)
            {
                List<Task<long>> taskList = new List<Task<long>>();
                for (int i = 0; i < uriArray.Length; i++)
                {
                    taskList.Add(GetTimeOut(uriArray[i]));
                }
    
                Task.WaitAll(taskList.ToArray());
    
                for (int i = 0; i < taskList.Count; i++)
                {
                    Console.WriteLine(uriArray[i].ToString() + ": " + taskList[i].Result);
                }
            }



    • 已编辑 lapheal 2014年4月21日 11:04
    2014年4月21日 10:51
  • 感谢 lapheal 的回复

    我用你的方法实际运行了一下,从语法上没有错误,但有两个问题:

    1. 这两种写法所产生的 Task 对象在初始化后,都有 Status 属性为“WaitingForActivation”的。第一种写法里边,有运行完成为“RanToCompletion”的,也有“WaitingForActivation”的,所以导致一直 Wait()在那里,而第二种写法,全部都是“WaitingForActivation”,似乎都没有运行

    2. 两种方法都会因为 Wait 而阻塞 UI,这是因为调用 TestSpeed 方法的线程是 UI 线程吗?如果是这样,岂不是异步也无法解决 Wait 引起的阻塞问题,难道需要单独开一个 Thread 来运行?

    谢谢


    不重要的其实最重要

    2014年4月22日 8:03
  • 感谢 lapheal 的回复

    我用你的方法实际运行了一下,从语法上没有错误,但有两个问题:

    1. 这两种写法所产生的 Task 对象在初始化后,都有 Status 属性为“WaitingForActivation”的。第一种写法里边,有运行完成为“RanToCompletion”的,也有“WaitingForActivation”的,所以导致一直 Wait()在那里,而第二种写法,全部都是“WaitingForActivation”,似乎都没有运行

    2. 两种方法都会因为 Wait 而阻塞 UI,这是因为调用 TestSpeed 方法的线程是 UI 线程吗?如果是这样,岂不是异步也无法解决 Wait 引起的阻塞问题,难道需要单独开一个 Thread 来运行?

    谢谢


    不重要的其实最重要

    不应该在UI上直接调用TestSpeed函数,否则会导致结果不正确,而应该用一个新的Task来调用,比如,假设你要在button1的点击事件里面调用,则:

    private async void button1_Click(object sender, EventArgs e)
    {
        Uri[] uris = new Uri[]
        {
            new Uri("http://www.baidu.com"),
            new Uri("http://www.sina.com.cn"),
            new Uri("http://www.google.com"),
        };
    
        await Task.Run(() => TestSpeed(uris));
    }

    如果你需要获取TestSpeed的结果并显示在UI上,可以把TestSpeed修改如下:

    public long[] TestSpeed(Uri[] uriArray)
    {
        long[] results = new long[uriArray.Length];
    
        Parallel.For(0, uriArray.Length, i =>
        {
            Task<long> task = GetTimeOut(uriArray[i]);
            task.Wait();
            results[i] = task.Result;
        });
    
        return results;
    }

    然后在调用时(假设显示在label1上):

    private async void button1_Click(object sender, EventArgs e)
    {
        Uri[] uris = new Uri[]
        {
            new Uri("http://www.baidu.com"),
            new Uri("http://www.sina.com.cn"),
            new Uri("http://www.google.com"),
        };
    
        long[] result = await Task<long[]>.Run(() => TestSpeed(uris));
        label1.Text = String.Join(",", result);
    }
    当然,这只是示例代码而已,根据你的需求修改即可。

    2014年4月22日 10:04
  • 非常感谢 lapheal 的回复。

    你的两次帮助又快有准,不仅写出了具体代码让我能看懂,而且还解决了我逻辑不清楚的地方。非常感谢!!!


    不重要的其实最重要

    2014年4月23日 5:30