【问题标题】:Tasks are starting sequencial instead of parallel任务开始顺序而不是并行
【发布时间】:2016-11-17 03:26:43
【问题描述】:

我需要并行启动任务,但我选择使用Task.Run 而不是Parallel.Foreach,这样我可以在所有任务完成后获得一些反馈并启用 UI 控件。

private async void buttonStart_Click(object sender, EventArgs e)
{
    var cells = objectListView.CheckedObjects;
    if(cells != null)
    {
        List<Task> tasks = new List<Task>();
        foreach (Cell c in cells)
        {
            Cell cell = c;
            var progressHandler = new Progress<string>(value =>
            {
                cell.Status = value;
            });
            var progress = progressHandler as IProgress<string>;

            Task t = Task.Run(() =>
            {
                progress.Report("Starting...");
                int a = 123;
                for (int i = 0; i < 200000; i++)
                {
                    a = a + i;
                    Task.Delay(500).Wait();
                }

                progress.Report("Done");
            });
            tasks.Add(t);

        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Done, enabld UI controls");
    }
}

所以我期望在 UI 中几乎立即看到所有项目的“正在启动...”。我实际看到的是前 4 个项目是“正在启动...”(我猜是因为每个线程都使用了所有 4 个 CPU 内核),然后每秒或更少的新项目是“启动”。我总共有 37 个项目,所有项目启动所有任务大约需要 30 秒。

我怎样才能让它尽可能平行?

【问题讨论】:

  • 使用一个并行的 foreach,因为你一次开始一个..
  • 当您看到应用程序这样运行时,您是如何运行的?你有 VS 附加吗?
  • 答案是你不能。你不能让它比你拥有的内核更多地并行。虽然你可以产生多个线程
  • @SriramSakthivel 他说他有 4 个内核并想要 4 个线程?
  • @Liam 他的任务不会很快完成。因此 ThreadPool 将使用其启发式添加线程。需要花时间。看起来他的 4 个内核正在被使用,但他通过进入睡眠状态来浪费线程。

标签: c# parallel-processing async-await task-parallel-library


【解决方案1】:

我怎样才能让它尽可能平行?

内部for循环的部分是模拟长时间运行的CPU密集型作业,我希望尽可能同时开始。

它已经尽可能并行了。启动 37 个需要 CPU 密集型工作的线程不会使其运行得更快,因为您显然是在 4 核机器上运行它。有 4 个内核,因此一次实际上只能运行 4 个线程。其他 33 个线程将等待,而 4 个线程正在运行。它们只会看起来同时运行。

也就是说,如果你真的想要启动所有这些线程池线程,你可以通过调用ThreadPool.SetMinThreads来做到这一点。

我需要并行启动任务,但我选择使用 Task.Run 而不是 Parallel.Foreach,因此我可以在所有任务完成后获得一些反馈并启用 UI 控件。

由于您有并行工作要做,您应该使用Parallel。如果您想要 await 的良好的恢复 UI 线程行为,那么您可以使用单个 await Task.Run,如下所示:

private async void buttonStart_Click(object sender, EventArgs e)
{
  var cells = objectListView.CheckedObjects;
  if (cells == null)
    return;

  var workItems = cells.Select(c => new
  {
    Cell = c,
    Progress = new Progress<string>(value => { c.Status = value; }),
  }).ToList();

  await Task.Run(() => Parallel.ForEach(workItems, item =>
  {
    var progress = item.Progress as IProgress<string>();
    progress.Report("Starting...");
    int a = 123;
    for (int i = 0; i < 200000; i++)
    {
      a = a + i;
      Thread.Sleep(500);
    }

    progress.Report("Done");
  }));

  Console.WriteLine("Done, enabld UI controls");
}

【讨论】:

  • 如果我需要将逻辑延迟(Task.Delay)放入项目的工作中怎么办?我必须制作异步 lambda 吗?我已经尝试并得到了奇怪的结果...Console.Write 发生在所有任务完成之前。
  • @Pablo:不,你不应该在Parallel 中使用async。为什么需要延迟?
  • 在现实生活中的每个项目作业中,我需要启动一些操作(CPU-Bound,但很短),然后每秒轮询一次反馈。
  • @Pablo:既然你有 CPU 密集型和异步工作要做,我建议使用 TPL 数据流而不是 Parallel。
  • 您的意思是创建一个带有动作块的管道,一个用于 CPU 密集型作业,另一个用于反馈?我可能需要尝试一下并发布一个新问题,以确保我走在正确的轨道上。
【解决方案2】:

我会说,它尽可能平行。如果您有 4 个内核,则可以并行运行 4 个线程。

如果您可以在等待“延迟”的同时做一些事情,请查看异步编程(一个线程可以“一次”运行多个任务,因为它们中的大多数都在等待某事)。

编辑:您也可以在自己的任务中运行Parallel.ForEach 和await:

private async void buttonStart_Click(object sender, EventArgs e)
{
    var cells = objectListView.CheckedObjects;
    if(cells != null)
    {
        await Task.Run( () => Parallel.ForEach( cells, c => ... ) );
    }
}

【讨论】:

  • Parallel.ForEach 给出了相同的结果,甚至更糟。唯一改变的是顺序。为了让Task.Delay 有机会暂停特定任务以便其他任务可以使用线程,我会改变什么?我猜.Wait() 部分不会让这种情况发生?
  • 好吧,如果你让你的任务异步并且await Task.Delay而不是等待,它根本不会等待。但是,如果您访问异步 api,则只能将其转移到现实世界的场景中,例如wcf 服务。如果Task.Delay().Wait() 是实际(cpu-)工作的占位符,那么您就不走运了 - 无论您启动多少线程,4 个内核都只会做一定数量的工作。实际上,您启动的线程越多,它们所做的事情就会越来越少,因为每个线程都会引入开销。
  • 如果你的任务做异步工作,你不会想使用Parallel.ForEach,虽然...
【解决方案3】:

我认为这取决于您的任务创建选项。

TaskCreationOptions.LongRunning

在这里您可以找到更多信息:

https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcreationoptions(v=vs.110).aspx

但是您必须知道,该任务使用具有有限最大线程数的线程池。您可以使用 LongRunning 发出信号,表明此任务需要很长时间并且不应阻塞您的池。我认为创建一个长时间运行的任务更复杂,因为调度程序可能会创建一个新线程。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TaskTest
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var demo = new Program();
            demo.SimulateClick();
            Console.ReadLine();
        }

        public void SimulateClick()
        {
            buttonStart_Click(null, null);
        }

        private async void buttonStart_Click(object sender, EventArgs e)
        {
            var tasks = new List<Task>();
            for (var i = 0; i < 36; i++)
            {
                var taskId = i;
                var t = Task.Factory.StartNew((() =>
                {
                    Console.WriteLine($"Starting Task ({taskId})");
                    for (var ii = 0; ii < 200000; ii++)
                    {
                        Task.Delay(TimeSpan.FromMilliseconds(500)).Wait();
                        var s1 = new string(' ', taskId);
                        var s2 = new string(' ', 36-taskId);
                        Console.WriteLine($"Updating Task {s1}X{s2} ({taskId})");
                    }
                    Console.Write($"Done ({taskId})");
                }),TaskCreationOptions.LongRunning);
                tasks.Add(t);
            }
            await Task.WhenAll(tasks);
            Console.WriteLine("Done, enabld UI controls");
        }
    }
}

【讨论】:

  • 这确实给出了预期的结果,因为它产生了与我的任务一样多的线程。只是想知道我是否可以对Task.Run 或Parallel.ForEach 做同样的事情...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-19
  • 2018-06-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多