【问题标题】:When executing an array of tasks asynchronously, shouldn't it take as long as the longest running task?异步执行一组任务时,不应该和运行时间最长的任务一样长吗?
【发布时间】:2017-05-26 00:25:44
【问题描述】:

我有一个包含 10 个任务的列表,每个任务需要 15 秒。所有任务都在一个数组中并且异步执行。整个集合不应该花费大约 15 秒吗?从下面的代码中,请注意在输出中整个集合需要 21 秒。当我将其更改为 100 个任务时,需要一分钟多的时间。就好像简单地创建任务需要一秒钟。我错过了什么?谢谢!

static void Main(string[] args)
{
    var mainStart = DateTime.Now;
    var tasks = new List<Task>();
    for (int i = 0; i < 10; i++)
    {
        tasks.Add(Task.Factory.StartNew((Object data) =>
        {
            var index = (int)data;
            var stepStart = DateTime.Now;
            Console.WriteLine("{0} Starting {1} on thread {2}...", stepStart, index, Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(15000);
            var stepFinish = DateTime.Now;
            Console.WriteLine("{0} Finished {1} on thread {2}, duration: {3}",
                stepStart, index, Thread.CurrentThread.ManagedThreadId, stepFinish - stepStart);
        },
        i));
    }
    Task.WaitAll(tasks.ToArray());
    var mainFinish = DateTime.Now;
    Console.WriteLine("{0} Finished, duration {1}", DateTime.Now, mainFinish - mainStart);
    Console.WriteLine("Press any key to exit.");
    Console.Read();

    // Output
    //5/25/2017 8:03:43 PM Starting 0 on thread 10...
    //5/25/2017 8:03:43 PM Starting 1 on thread 11...
    //5/25/2017 8:03:43 PM Starting 2 on thread 12...
    //5/25/2017 8:03:43 PM Starting 3 on thread 13...
    //5/25/2017 8:03:44 PM Starting 4 on thread 14...
    //5/25/2017 8:03:45 PM Starting 5 on thread 15...
    //5/25/2017 8:03:46 PM Starting 6 on thread 16...
    //5/25/2017 8:03:47 PM Starting 7 on thread 17...
    //5/25/2017 8:03:48 PM Starting 8 on thread 18...
    //5/25/2017 8:03:49 PM Starting 9 on thread 19...
    //5/25/2017 8:03:43 PM Finished 0 on thread 10, duration: 00:00:15.0018957
    //5/25/2017 8:03:43 PM Finished 1 on thread 11, duration: 00:00:15.0175209
    //5/25/2017 8:03:43 PM Finished 2 on thread 12, duration: 00:00:15.0175209
    //5/25/2017 8:03:43 PM Finished 3 on thread 13, duration: 00:00:15.0165291
    //5/25/2017 8:03:44 PM Finished 4 on thread 14, duration: 00:00:15.0156567
    //5/25/2017 8:03:45 PM Finished 5 on thread 15, duration: 00:00:15.0156012
    //5/25/2017 8:03:46 PM Finished 6 on thread 16, duration: 00:00:15.0155997
    //5/25/2017 8:03:47 PM Finished 7 on thread 17, duration: 00:00:15.0155989
    //5/25/2017 8:03:48 PM Finished 8 on thread 18, duration: 00:00:15.0155985
    //5/25/2017 8:03:49 PM Finished 9 on thread 19, duration: 00:00:15.0156328
    //5/25/2017 8:04:04 PM Finished, duration 00:00:21.0322775
    //Press any key to exit.
}

【问题讨论】:

  • 谢谢@Clay 和@Guilherme!两个答案都很棒,每个都可以根据情况细节使用。我将两者标记为答案和有用,不知道在这个论坛中会如何对待它们。

标签: c# multithreading asynchronous task


【解决方案1】:

名义上,异步工作允许其他事情在等待某事时继续进行……他们这样做的方式是等待耗时的事情(这通常是某种 I/O )。为了看到实际效果,您可以以真正异步的方式运行工作。例如:

static void Main( string[ ] args )
{
  var totalTime = DoSomeAsyncTasks( ).GetAwaiter( ).GetResult( );
  Console.WriteLine( "{0} Finished, duration {1}", DateTime.Now, totalTime );
  Console.WriteLine( "Press any key to exit." );
  Console.Read( );
}

async static Task<TimeSpan> DoSomeAsyncTasks( )
{
  var mainStart = DateTime.Now;
  var tasks = new List<Task>( );
  for ( int i = 0; i < 10; i++ )
  {
    var id = i;
    tasks.Add( DoATask( id ) );
  }
  await Task.WhenAll( tasks );
  var mainFinish = DateTime.Now;
  return mainFinish - mainStart;
}

static async Task DoATask( int stepId )
{
  var stepStart = DateTime.Now;

  Console.WriteLine(
    "{0} Starting {1} on thread {2}...",
    stepStart, stepId, Thread.CurrentThread.ManagedThreadId );

  //--> more accurately model waiting for I/O...
  await Task.Delay( TimeSpan.FromSeconds( 15 ) );

  var stepFinish = DateTime.Now;
  Console.WriteLine( "{0} Finished {1} on thread {2}, duration: {3}",
      stepStart, stepId, Thread.CurrentThread.ManagedThreadId, stepFinish - stepStart );
}

...返回:

5/25/2017 11:24:36 PM Starting 0 on thread 9...
5/25/2017 11:24:36 PM Starting 1 on thread 9...
5/25/2017 11:24:36 PM Starting 2 on thread 9...
5/25/2017 11:24:36 PM Starting 3 on thread 9...
5/25/2017 11:24:36 PM Starting 4 on thread 9...
5/25/2017 11:24:36 PM Starting 5 on thread 9...
5/25/2017 11:24:36 PM Starting 6 on thread 9...
5/25/2017 11:24:36 PM Starting 7 on thread 9...
5/25/2017 11:24:36 PM Starting 8 on thread 9...
5/25/2017 11:24:36 PM Starting 9 on thread 9...
5/25/2017 11:24:36 PM Finished 9 on thread 11, duration: 00:00:15.0085175
5/25/2017 11:24:36 PM Finished 8 on thread 12, duration: 00:00:15.0085175
5/25/2017 11:24:36 PM Finished 7 on thread 13, duration: 00:00:15.0315198
5/25/2017 11:24:36 PM Finished 6 on thread 14, duration: 00:00:15.0325121
5/25/2017 11:24:36 PM Finished 5 on thread 12, duration: 00:00:15.0335121
5/25/2017 11:24:36 PM Finished 3 on thread 11, duration: 00:00:15.0335121
5/25/2017 11:24:36 PM Finished 2 on thread 12, duration: 00:00:15.0355229
5/25/2017 11:24:36 PM Finished 1 on thread 11, duration: 00:00:15.0355229
5/25/2017 11:24:36 PM Finished 4 on thread 14, duration: 00:00:15.0335121
5/25/2017 11:24:36 PM Finished 0 on thread 13, duration: 00:00:15.0545213
5/25/2017 11:24:51 PM Finished, duration 00:00:15.0665191

(我做了所有与您的代码相同的事情 - 并且接受的答案确实...只是将其分散到方法中以使其更具说明性。)

您应该从中看到,每个任务大约在同一时间开始,每个任务大约需要 15 秒,但总运行时间也在 15 秒左右。这是因为,当一个任务在等待时,它让其他工作运行。另请注意,所有内容都在同一个单线程上运行。工作与等待交织在一起。这很酷 - 可能更像您所期待的。

【讨论】:

    【解决方案2】:

    任务不是线程。如果您想保证所有这些任务同时并行运行,请尝试以下方法:

    class Program
    {
        static void Main(string[] args)
        {
            var mainStart = DateTime.Now;
            var threads = new List<Thread>();
            for (int i = 0; i < 10; i++)
            {
                threads.Add(new Thread(() =>
                {
                    var stepStart = DateTime.Now;
                    Console.WriteLine("{0} Starting {1} on thread {2}...", stepStart, i, Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(15000);
                    var stepFinish = DateTime.Now;
                    Console.WriteLine("{0} Finished {1} on thread {2}, duration: {3}",
                        stepStart, i, Thread.CurrentThread.ManagedThreadId, stepFinish - stepStart);
                }));
            }
    
            foreach (Thread t in threads)
            {
                t.Start(); // Starts all the threads
            }
    
            foreach(Thread t in threads)
            {
                t.Join(); // Make the main thread wait for the others
            }
    
            var mainFinish = DateTime.Now;
            Console.WriteLine("{0} Finished, duration {1}", DateTime.Now, mainFinish - mainStart);
            Console.WriteLine("Press any key to exit.");
            Console.Read();
        }
    }
    

    阅读更多here 了解主要区别。

    正如乔恩·斯基特所说:

    线程是一个低级概念:如果你直接启动一个线程,你就知道它将是一个单独的线程,而不是在线程池等上执行。

    任务不仅仅是“在哪里运行一些代码”的抽象——它实际上只是“对未来结果的承诺”。

    【讨论】:

      【解决方案3】:

      我错过了什么?

      线程池的工作原理。

      您可以将线程池​​视为线程的集合,以及要完成的工作队列。通常,线程池的线程集合与 CPU 内核的数量大致相同 - 因为一次只有那么多线程可以实际执行(即运行代码)。

      此外,当线程池的工作比线程多时,它会向其线程集合添加更多线程。但它限制了线程注入率 - IIRC,当前线程注入率大约是每 2 秒一个新线程。这种限制对于防止线程抖动是必要的;创建和销毁线程的成本很高,因此线程池使用注入速率限制作为启发式方法。

      所以,one answer 在这里通过使用普通线程来避免线程池(在实际代码中我绝不会推荐这种方式)。 Another answer 通过使用异步任务来避免线程池。这个答案只是解释了为什么你会看到这种行为。

      但如果您想在线程池上同时(和同步地)运行它们,您可以通过将线程池告知increase its minimum number of threads 来实现。

      【讨论】:

        【解决方案4】:

        Factory.StartNew 只是在TaskScheduler 上将一个任务排入队列——如果它的所有线程都忙于其他工作,它不一定会立即运行该任务。如果您记录开始时间,那么您会看到其中至少一项任务在 6 秒内没有开始。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-05-23
          • 2022-01-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多