C# 继续任务上的Task.WaitAll()仅延迟原始任务的执行?

C# 继续任务上的Task.WaitAll()仅延迟原始任务的执行?,c#,.net,multithreading,task-parallel-library,C#,.net,Multithreading,Task Parallel Library,背景: 我有一个控制台应用程序,它可以创建任务来处理数据库中的数据(我们称之为Level1任务)。每个任务都会再次创建自己的任务,以处理分配给它的数据的每个部分(级别2任务) 每个Level2任务都有一个与其关联的延续任务,代码用于在继续之前对延续任务执行WaitAll 我在.net4.0(无async/wait) 问题: 但这产生了一个问题——结果是,如果采用这种方式,则在计划所有可用的级别1任务之前,没有一个级别2任务启动。这在任何方面都不是最优的 问题: 通过将代码更改为等待原始的Leve

背景:

我有一个控制台应用程序,它可以创建
任务
来处理数据库中的数据(我们称之为Level1任务)。每个任务都会再次创建自己的任务,以处理分配给它的数据的每个部分(级别2任务)

每个Level2任务都有一个与其关联的延续任务,代码用于在继续之前对延续任务执行
WaitAll

我在
.net4.0
(无
async
/
wait

问题:

但这产生了一个问题——结果是,如果采用这种方式,则在计划所有可用的级别1任务之前,没有一个级别2任务启动。这在任何方面都不是最优的

问题:

通过将代码更改为等待原始的Level2任务及其延续任务,似乎可以解决这一问题。然而,我不完全确定为什么会这样

你有什么想法吗

我能想到的唯一一件事是——既然延续任务还没有开始,就没有必要等待它完成。但即使是这样,我也希望至少有一些2级任务已经开始。他们从来没有这样做过

示例:

我创建了一个示例控制台应用程序,正好演示了这种行为:

  • 按原样运行它,您将看到它首先安排所有任务,然后才开始从Level2任务中写入实际行

  • 但是注释掉标记的代码块,取消对替换和所有工作的注释

  • 你能告诉我为什么吗

    public class Program
    {
        static void Main(string[] args)
        {
            for (var i = 0; i < 100; i++)
            {
                Task.Factory.StartNew(() => SomeMethod());
                //Thread.Sleep(1000);
            }
    
            Console.ReadLine();
        }
    
        private static void SomeMethod()
        {
            var numbers = new List<int>();
    
            for (var i = 0; i < 10; i++)
            {
                numbers.Add(i);
            }
    
            var tasks = new List<Task>();
    
            foreach (var number in numbers)
            {
                Console.WriteLine("Before start task");
    
                var numberSafe = number;
    
                /* Code to be replaced START */
    
                var nextTask = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Got number: {0}", numberSafe);
                })
                    .ContinueWith(task =>
                    {
                        Console.WriteLine("Continuation {0}", task.Id);
                    });
    
                tasks.Add(nextTask);
    
                /* Code to be replaced END */
    
                /* Replacement START */
    
                //var originalTask = Task.Factory.StartNew(() =>
                //{
                //    Console.WriteLine("Got number: {0}", numberSafe);
                //});
    
                //var contTask = originalTask
                //    .ContinueWith(task =>
                //    {
                //        Console.WriteLine("Continuation {0}", task.Id);
                //    });
    
                //tasks.Add(originalTask);
                //tasks.Add(contTask);
    
                /* Replacement END */
            }
    
            Task.WaitAll(tasks.ToArray());
        }
    }
    
    公共类程序
    {
    静态void Main(字符串[]参数)
    {
    对于(变量i=0;i<100;i++)
    {
    Task.Factory.StartNew(()=>SomeMethod());
    //睡眠(1000);
    }
    Console.ReadLine();
    }
    私有静态方法()
    {
    变量编号=新列表();
    对于(变量i=0;i<10;i++)
    {
    增加(i);
    }
    var tasks=新列表();
    foreach(数值中的变量编号)
    {
    Console.WriteLine(“启动前任务”);
    var numberSafe=数量;
    /*要替换的代码开始*/
    var nextTask=Task.Factory.StartNew(()=>
    {
    WriteLine(“获取编号:{0}”,numberSafe);
    })
    .ContinueWith(任务=>
    {
    WriteLine(“Continuation{0}”,task.Id);
    });
    任务。添加(nextTask);
    /*待替换代码结束*/
    /*更换起动*/
    //var originalTask=Task.Factory.StartNew(()=>
    //{
    //WriteLine(“获取编号:{0}”,numberSafe);
    //});
    //var contTask=originalTask
    //.ContinueWith(任务=>
    //    {
    //WriteLine(“Continuation{0}”,task.Id);
    //    });
    //任务。添加(originalTask);
    //任务。添加(contTask);
    /*替换端*/
    }
    Task.WaitAll(tasks.ToArray());
    }
    }
    
    我必须说,这段代码确实不乐观,因为您创建了100个任务,但这并不意味着您将有100个线程,并且在每个任务中创建了两个新任务,您对调度程序的分配过多。如果这些任务与数据库读取相关,为什么不将它们标记为长处理并放弃内部任务?

    我认为您看到了
    任务内联行为。引述自:

    在某些情况下,当任务处于等待状态时,它可能会在执行等待操作的线程上同步执行。这提高了性能,因为它通过利用现有线程(否则会被阻塞)来防止需要额外的线程。为了防止由于重新进入而导致错误,只有在相关线程的本地队列中找到等待目标时,才会发生任务内联

    你不需要100个任务就能看到这一点。我已经修改了你的程序,使其具有4个1级任务(我有四核CPU)。每个级别1任务仅创建一个级别2任务

    static void Main(string[] args)
    {
        for (var i = 0; i < 4; i++)
        {
            int j = i;
            Task.Factory.StartNew(() => SomeMethod(j)); // j as level number
        }
    }
    
    下面是示例输出-关于注释
    tasks.Add(originalTask)-这是您的第一个块

    Before start task: 0 - thread 4
    Before start task: 2 - thread 3
    Before start task: 3 - thread 6
    Before start task: 1 - thread 5
    Got number: 0 - thread 7
    Continuation 0 - thread 7
    Got number: 1 - thread 7
    Continuation 1 - thread 7
    Got number: 3 - thread 7
    Continuation 3 - thread 7
    Got number: 2 - thread 4
    Continuation 2 - thread 4
    
    和一些示例输出-关于保持
    任务.Add(originalTask)哪一个是您的第二个块

    Before start task: 0 - thread 4
    Before start task: 1 - thread 6
    Before start task: 2 - thread 5
    Got number: 0 - thread 4
    Before start task: 3 - thread 3
    Got number: 3 - thread 3
    Got number: 1 - thread 6
    Got number: 2 - thread 5
    Continuation 0 - thread 7
    Continuation 1 - thread 7
    Continuation 3 - thread 7
    Continuation 2 - thread 4
    

    正如您在第二种情况下看到的,当您在启动它的同一线程上等待
    originalTask
    时,
    任务内联将使它在同一线程上运行-这就是为什么您在前面看到
    get Number..
    消息的原因。

    如果我没记错的话,等待尚未计划的任务可能会同步执行。(请参阅)在另一种情况下,这种行为将应用于您的代码也就不足为奇了

    请记住,线程行为是高度依赖于实现和机器的,这里发生的事情可能是这样的:

    • 考虑到对线程池的调用和任务的实际执行之间的延迟,大多数所谓的“级别1”任务(如果不是全部的话)都是在第一个任务实际执行之前安排的
    • 由于默认任务计划程序使用.NET线程池,因此此处计划的所有任务都可能在线程池线程上执行
    • 一旦执行了“1级”任务,调度队列中就会充满“1级”任务
    • 每次执行“1级”任务时,它都会根据需要安排尽可能多的“2级”任务,但这些任务都安排在“1级”任务之后
    • 当“级别1”任务到达等待“级别2”任务的所有延续的点时,执行线程进入等待状态
    • 有许多线程池
      Before start task: 0 - thread 4
      Before start task: 1 - thread 6
      Before start task: 2 - thread 5
      Got number: 0 - thread 4
      Before start task: 3 - thread 3
      Got number: 3 - thread 3
      Got number: 1 - thread 6
      Got number: 2 - thread 5
      Continuation 0 - thread 7
      Continuation 1 - thread 7
      Continuation 3 - thread 7
      Continuation 2 - thread 4
      
      static void Main(string[] args)
      {
          for (var i = 0; i < 100; i++)
          {
              Task.Factory.StartNew(() => SomeMethod(), 
                  TaskCreationOptions.LongRunning);
          }
      
          Console.ReadLine();
      }
      
      using System;
      using System.Collections.Generic;
      using System.Threading.Tasks;
      
      public class Program
      {
          static void Main(string[] args)
          {
              var tasks = new List<Task>();
      
              for (var i = 0; i < 100; i++)
              {
                  tasks.Add(Task.Factory.StartNew(() => SomeMethod(i)).Unwrap());
              }
      
              // blocking at the topmost level
              Task.WaitAll(tasks.ToArray());
      
              Console.WriteLine("Enter to exit...");
              Console.ReadLine();
          }
      
          private static Task<Task[]> SomeMethod(int n)
          {
              Console.WriteLine("SomeMethod " + n);
      
              var numbers = new List<int>();
      
              for (var i = 0; i < 10; i++)
              {
                  numbers.Add(i);
              }
      
              var tasks = new List<Task>();
      
              foreach (var number in numbers)
              {
                  Console.WriteLine("Before start task " + number);
      
                  var numberSafe = number;
      
                  var nextTask = Task.Factory.StartNew(() =>
                  {
                      Console.WriteLine("Got number: {0}", numberSafe);
                  })
                  .ContinueWith(task =>
                  {
                      Console.WriteLine("Continuation {0}", task.Id);
                  }, TaskContinuationOptions.ExecuteSynchronously);
      
                  tasks.Add(nextTask);
              }
      
              return Task.Factory.ContinueWhenAll(tasks.ToArray(), 
                  result => result, TaskContinuationOptions.ExecuteSynchronously);
          }
      }