C# Task.Factory.StartNew与Parallel.Invoke

C# Task.Factory.StartNew与Parallel.Invoke,c#,parallel-processing,C#,Parallel Processing,在我的应用程序中,我并行执行几十到几百个动作(这些动作没有返回值) 哪种方法最为理想: 在foreach循环中使用Task.Factory.StartNew,迭代Action数组(Action[]) Task.Factory.StartNew(()=>someAction()) 使用Parallel类,其中actions是Action数组(Action[]) Parallel.Invoke(操作) 这两种方法等效吗?是否存在任何性能影响 编辑 我已经进行了一些性能测试,在我的机器上(每个2个CP

在我的应用程序中,我并行执行几十到几百个动作(这些动作没有返回值)

哪种方法最为理想:

  • 在foreach循环中使用
    Task.Factory.StartNew
    ,迭代
    Action
    数组(
    Action[]

    Task.Factory.StartNew(()=>someAction())

  • 使用
    Parallel
    类,其中
    actions
    Action
    数组(
    Action[]

    Parallel.Invoke(操作)

  • 这两种方法等效吗?是否存在任何性能影响

    编辑


    我已经进行了一些性能测试,在我的机器上(每个2个CPU 2个内核),结果似乎非常相似。我不确定它在其他机器上会是什么样子,比如1CPU。此外,我也不确定(不知道如何非常准确地测试)内存消耗是什么。

    从总体来看,考虑到在任何情况下实际处理大量任务的开销,这两种方法之间的性能差异可以忽略不计

    Parallel.Invoke
    基本上为您执行
    Task.Factory.StartNew()
    。所以,我认为可读性在这里更重要


    此外,正如StriplingWarrior提到的,
    Parallel.Invoke
    为您执行一个
    WaitAll
    (在所有任务完成之前阻塞代码),因此您也不必这样做。如果希望在后台运行任务而不关心任务何时完成,则需要
    Task.Factory.StartNew()

    这两者之间最重要的区别是
    并行。Invoke
    将等待所有操作完成后再继续执行代码,而
    StartNew
    将转到下一行代码,允许任务在自己的适当时间完成

    这种语义上的差异应该是你首先(可能也是唯一)考虑的。但为了便于参考,这里有一个基准:

    /* This is a benchmarking template I use in LINQPad when I want to do a
     * quick performance test. Just give it a couple of actions to test and
     * it will give you a pretty good idea of how long they take compared
     * to one another. It's not perfect: You can expect a 3% error margin
     * under ideal circumstances. But if you're not going to improve
     * performance by more than 3%, you probably don't care anyway.*/
    void Main()
    {
        // Enter setup code here
        var actions2 =
        (from i in Enumerable.Range(1, 10000)
        select (Action)(() => {})).ToArray();
    
        var awaitList = new Task[actions2.Length];
        var actions = new[]
        {
            new TimedAction("Task.Factory.StartNew", () =>
            {
                // Enter code to test here
                int j = 0;
                foreach(var action in actions2)
                {
                    awaitList[j++] = Task.Factory.StartNew(action);
                }
                Task.WaitAll(awaitList);
            }),
            new TimedAction("Parallel.Invoke", () =>
            {
                // Enter code to test here
                Parallel.Invoke(actions2);
            }),
        };
        const int TimesToRun = 100; // Tweak this as necessary
        TimeActions(TimesToRun, actions);
    }
    
    
    #region timer helper methods
    // Define other methods and classes here
    public void TimeActions(int iterations, params TimedAction[] actions)
    {
        Stopwatch s = new Stopwatch();
        int length = actions.Length;
        var results = new ActionResult[actions.Length];
        // Perform the actions in their initial order.
        for(int i = 0; i < length; i++)
        {
            var action = actions[i];
            var result = results[i] = new ActionResult{Message = action.Message};
            // Do a dry run to get things ramped up/cached
            result.DryRun1 = s.Time(action.Action, 10);
            result.FullRun1 = s.Time(action.Action, iterations);
        }
        // Perform the actions in reverse order.
        for(int i = length - 1; i >= 0; i--)
        {
            var action = actions[i];
            var result = results[i];
            // Do a dry run to get things ramped up/cached
            result.DryRun2 = s.Time(action.Action, 10);
            result.FullRun2 = s.Time(action.Action, iterations);
        }
        results.Dump();
    }
    
    public class ActionResult
    {
        public string Message {get;set;}
        public double DryRun1 {get;set;}
        public double DryRun2 {get;set;}
        public double FullRun1 {get;set;}
        public double FullRun2 {get;set;}
    }
    
    public class TimedAction
    {
        public TimedAction(string message, Action action)
        {
            Message = message;
            Action = action;
        }
        public string Message {get;private set;}
        public Action Action {get;private set;}
    }
    
    public static class StopwatchExtensions
    {
        public static double Time(this Stopwatch sw, Action action, int iterations)
        {
            sw.Restart();
            for (int i = 0; i < iterations; i++)
            {
                action();
            }
            sw.Stop();
    
            return sw.Elapsed.TotalMilliseconds;
        }
    }
    #endregion
    

    如您所见,使用Parallel.Invoke比等待一堆新任务完成快大约4.5倍。当然,那是你的行为毫无作用的时候。每个动作做得越多,您会注意到的差异就越小。

    我使用了StriplingWarror的测试来找出差异的来源。我这样做是因为当我使用Reflector查看代码时,类并行所做的只是创建一组任务并让它们运行

    从理论上看,这两种方法在运行时方面应该是等效的。但是,使用空操作进行的测试(不太现实)表明,并行类的速度要快得多

    任务版本几乎所有的时间都花在创建新任务上,这会导致许多垃圾收集。您看到的速度差异纯粹是由于您创建了许多任务,这些任务很快就会变成垃圾

    相反,并行类创建自己的任务派生类,该类在所有CPU上并发运行。所有内核上只运行一个物理任务。同步现在确实发生在任务委托内部,这确实解释了并行类的更快速度。

    ParallelForReplicatingTask2=新建ParallelForReplicatingTask(parallelOptions,委托{
    
    对于(int k=联锁增量(参考动作索引);k我认为它们或多或少是等效的。你的分析器告诉你什么?你考虑过基准测试吗?看看这篇关于这两种方法的好文章:。我不认为有任何性能差异,但我认为并行。调用更简单。对于你描述的情况,我同意其他评论,因为它们基本上是相同的相同;但是,开始自己的任务会让你更好地控制事情,比如继续、单独取消等等,是吗?请看以下内容:
    Message               | DryRun1 | DryRun2 | FullRun1 | FullRun2
    ----------------------------------------------------------------
    Task.Factory.StartNew | 43.0592 | 50.847  | 452.2637 | 463.2310
    Parallel.Invoke       | 10.5717 |  9.948  | 102.7767 | 101.1158 
    
    ParallelForReplicatingTask task2 = new ParallelForReplicatingTask(parallelOptions, delegate {
            for (int k = Interlocked.Increment(ref actionIndex); k <= actionsCopy.Length; k = Interlocked.Increment(ref actionIndex))
            {
                actionsCopy[k - 1]();
            }
        }, TaskCreationOptions.None, InternalTaskOptions.SelfReplicating);
    task2.RunSynchronously(parallelOptions.EffectiveTaskScheduler);
    task2.Wait();