Task.WaitAll方法 vs Parallel.Invoke方法

35

我有一份样本代码,可以比较并行方法和任务方法的处理时间。实验的目的是了解它们的工作方式。

那么我的问题是:

  1. 为什么并行方法比任务方法更快?
  2. 我的结果是否意味着我应该使用并行而不是任务?
  3. 在哪些情况下应该使用任务和并行?
  4. 与使用并行相比,使用任务的好处是什么?
  5. 任务只是 ThreadPool.QueueUserWorkItem 方法的包装吗?

    public Task SomeLongOperation()
    {
        return Task.Delay(3000);
    }

    static void Main(string[] args)
    {
        Program p = new Program();
        List<Task> tasks = new List<Task>();

        tasks.Add(Task.Factory.StartNew(() => p.SomeLongOperation()));
        tasks.Add(Task.Factory.StartNew(() => p.SomeLongOperation()));

        var arr = tasks.ToArray();

        Stopwatch sw = Stopwatch.StartNew();
        Task.WaitAll(arr);
        Console.WriteLine("Task wait all results: " + sw.Elapsed);
        sw.Stop();

        sw = Stopwatch.StartNew();
        Parallel.Invoke(() => p.SomeLongOperation(), () => p.SomeLongOperation());
        Console.WriteLine("Parallel invoke results: " + sw.Elapsed);
        sw.Stop();

        Console.ReadKey();
    }

以下是我的处理结果: results

编辑:

将代码更改为以下内容:

    Program p = new Program();
    Task[] tasks = new Task[2];

    Stopwatch sw = Stopwatch.StartNew();
    tasks[0] = Task.Factory.StartNew(() => p.SomeLongOperation());
    tasks[1] = Task.Factory.StartNew(() => p.SomeLongOperation());

    Task.WaitAll(tasks);
    Console.WriteLine("Task wait all results: " + sw.Elapsed);
    sw.Stop();

    sw = Stopwatch.StartNew();
    Parallel.Invoke(() => p.SomeLongOperation(), () => p.SomeLongOperation());
    Console.WriteLine("Parallel invoke results: " + sw.Elapsed);
    sw.Stop();

我的新结果:

新结果

编辑2: 当我把代码替换为Parallel.Invoke排在第一位,Task.WaitAll排在第二位时,情况发生了彻底改变。现在Parallel的速度更慢了。这让我想到我的估计可能是不正确的。我把代码改成了这样:

Program p = new Program();
Task[] tasks = new Task[2];

Stopwatch sw = null;
for (int i = 0; i < 10; i++)
{
    sw = Stopwatch.StartNew();
    Parallel.Invoke(() => p.SomeLongOperation(), () => p.SomeLongOperation());
    string res = sw.Elapsed.ToString();
    Console.WriteLine("Parallel invoke results: " + res);
    sw.Stop();
}

for (int i = 0; i < 10; i++)
{
    sw = Stopwatch.StartNew();
    tasks[0] = Task.Factory.StartNew(() => p.SomeLongOperation());
    tasks[1] = Task.Factory.StartNew(() => p.SomeLongOperation());
    Task.WaitAll(tasks);
    string res2 = sw.Elapsed.ToString();
    Console.WriteLine("Task wait all results: " + res2);
    sw.Stop();
}

以下是我的新结果:

输入图像描述

输入图像描述

现在我可以建议这个实验更加清晰。结果几乎相同。 有时Parallel更快,有时Task更快。 现在我的问题是:

1. 在哪些情况下应该使用Task和Parallel?

2. 相比Parallel,使用Task的好处是什么?

3. Task只是ThreadPool.QueueUserWorkItem方法的包装吗?

欢迎提供任何有助于澄清这些问题的信息。


2
tasks.Add(Task.Factory.StartNew(() => p.SomeLongOperation())); 已经开始执行,但此时您的秒表还没有开始计时! - David
我应该如何更改代码,使这个实验更加清晰? - Igor Lozovsky
将它移动到startnew()的上方。 - David
顺便说一下:你可以调用sw.Elapsed.Ticks或sw.Elapsed.TotalMilliseconds,而不是使用'sw.Elapsed',前面的00:00...没有什么帮助。 - David
我将startnew方法移动到tasks.Add(...)的上面。现在tasks比之前要慢得多。 - Igor Lozovsky
可能是由于ToArray方法的调用而产生了开销? - Igor Lozovsky
2个回答

11
根据MSDN这篇文章,Parallel和Task都是ThreadPool的包装器。Parallel invoke会等待所有任务完成后才结束。
关于您的问题: 使用Task、Parallel或ThreadPool取决于您需要对并行任务执行有多细粒度的控制。我个人习惯于使用Task.Factory.StartNew(),但这只是个人意见。同样适用于ThreadPool.QueueUserWorkItem() 额外的信息:第一次调用Parallel.Invoke()和Task.Factory.StartNew()可能由于内部初始化而较慢。

我的结果总是几乎相同。无论如何,Task.WaitAll在任何方面都比Parallel.Invoke慢。也许我没有理解某些东西或这种比较是不正确的。无论如何,我希望得到答案来澄清这些方法。我认为对我的问题的回答也将对其他人有用。 - Igor Lozovsky
只是提供信息:SomeLongOperation()需要多少时间? - Stephen Reindl
3
阅读这篇MSDN文章,它说:“......在内部,Parallel.Invoke创建新的任务并等待它们。 它使用Task类的方法来实现这一点。 这是一个示例......” - Stephen Reindl
根据这篇文章,我的测试用例是一样的。感谢您的帮助。如果您找到了我所有问题的答案,请编辑您的回答,以便我可以将其标记为正确。 - Igor Lozovsky
请将您的答案编辑为方法StartNew,而不是StartNow。Parallel和Task都是ThreadPool的包装器。Parallel invoke还会等待所有任务完成后再继续执行。 - Igor Lozovsky
没有人应该使用 Task.Factory.StartNew() http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html - Chris Marisic

7
如果您启动非泛型任务(即“没有返回值的void任务”)并立即等待它们,请改用Parallel.Invoke。这样可以让读者立即明白您的意图。
如果需要实现以下功能,请使用Tasks: - 没有立即等待 - 需要返回值 - 需要向被调用的方法传递参数 - 需要TaskCreationOptions功能 - 需要CancellationToken或TaskScheduler功能,且不想使用ParallelOptions - 基本上,如果您想获得更多选项或控制权
是的,您可以绕过其中一些限制,例如Parallel.Invoke(() => p.OpWithToken(CancellationToken),但这会使您的意图变得难以理解。Parallel.Invoke用于尽可能地利用CPU执行大量工作。执行完成后,不会导致死锁,并且您事先知道这一点。
然而,您的测试很糟糕。红旗是您的长时间操作等待3000毫秒,但您的测试时间少于十分之一毫秒。
Task.Factory.StartNew(() => p.SomeLongOperation());

StartNew接受一个Action,并在新的Task中执行。动作() => SomeLongOperation()创建了一个subtask Task。在创建这个子任务后(未完成),调用SomeLongOperation()返回,并且Action已经完成。因此,在十毫秒后,main Task已经完成,而你没有任何关于正在后台运行的两个子任务的参考。Parallel路径也创建了两个子任务,但没有跟踪它们,然后就返回。

正确的方式是tasks[0] = p.SomeLongOperation();,它将一个正在运行的任务分配给数组。然后WaitAll检查此任务是否已经完成。


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