When executing an array of tasks asynchronously, s

2019-07-09 06:51发布

问题:

I have a list of 10 Tasks, each task takes 15 seconds. All the tasks are in an array and are executed asynchronously. Shouldn't the entire set take about 15 seconds? From the code below, notice that in the output the entire set takes 21 seconds. When I change it to 100 tasks, it takes over a minute. It's almost as if simply creating the task takes a second. What am I missing? Thanks!

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.
}

回答1:

Nominally, async work allows other things to proceed when they're waiting on something...and the way they do that is by awaiting the time-taking stuff (which is more-than-often some kind of I/O). In order to see that in action, you could run the work truly async fashion. For example:

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 );
}

...which returned:

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

(I did all the same things your code - and the accepted answer did...just spread it out into methods to make it a little more illustrative.)

What you should see from this is that each task started at about the same time, and each task took around 15 seconds, but the total runtime was also around 15 seconds. This is because, while a task was waiting, it let other work run. Also note than everything is running on the same, single thread. The work is interleaved by the awaiting. It's pretty cool - and probably more like what you might have been expecting.



回答2:

A task is not a thread. If you want to guarantee that all of those tasks run in parallel at the same time, try this instead:

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();
    }
}

Read more here about the main differences.

As Jon Skeet says:

Thread is a lower-level concept: if you're directly starting a thread, you know it will be a separate thread, rather than executing on the thread pool etc.

Task is more than just an abstraction of "where to run some code" though - it's really just "the promise of a result in the future".



回答3:

What am I missing?

How the thread pool works.

You can think of the thread pool as a collection of threads, along with a queue of work to be done. Normally, the thread pool's collection of threads is about the same as the number of CPU cores - because only that many threads can actually execute (i.e., run code) at a time.

Furthermore, when the thread pool has more work to do than threads, then it will add more threads to its thread collection. But it limits the thread injection rate - IIRC, the current thread injection rate is something like one new thread every 2 seconds. This limiting is necessary to prevent thread thrashing; it's expensive to create and destroy threads, so the thread pool uses injection rate limiting as a heuristic.

So, one answer here avoids the thread pool by using plain threads (something I'd never recommend in real-world code). Another answer avoids the thread pool by using asynchronous tasks. And this answer just explains why you were seeing that behavior.

But if you wanted to run them all simultaneously (and synchronously) on the thread pool, you can do that by telling the thread pool to increase its minimum number of threads.



回答4:

Factory.StartNew simply enqueues a task on the TaskScheduler--it doesn't necessarily run the task right away if all of its threads are busy with othe work. If you log the start time then you'll see that at least one of those tasks doesn't start for 6 seconds.