简体   繁体   中英

How to run two tasks in parallel with C#?

Suppose that I have this code that should be run two tasks correctly in parallel , I didn't have a good idea about parallels tasks in C# and I would like to just start with this code to understand this concept, what I would like to do is running two task in the same time ( asynchronously )

 public async Task Main() {
     var task1 = Task.Run(() => DoWork());
     var task2 = Task.Run(() => CleanIt());

     await Task.WhenAll(task1, task2);
 }

 private void CleanIt() {
     int sum = 0;
     for (int i = 0; i < 10; i++) {
         Console.WriteLine(" Thread two " + i);
     }
 }

 private void DoWork() {
     int sum = 0;
     for (int i = 0; i < 10; i++) {
         Console.WriteLine(" Thread one " + i);
     }
 }

The result that I've got:

 Thread one 0
 Thread two 0
 Thread one 1
 Thread one 2
 Thread one 3
 Thread one 4
 Thread one 5
 Thread one 6
 Thread one 7
 Thread one 8
 Thread one 9
 Thread two 1
 Thread two 2
 Thread two 3
 Thread two 4
 Thread two 5
 Thread two 6
 Thread two 7
 Thread two 8
 Thread two 9
  

I would like to show the result like this :

 Thread one 0
 Thread two 0
 Thread one 1
 Thread two 1
 Thread one 2
 Thread two 2
 ....

How can I achieve this result ?

The idea of async code is that you tell the processor not to wait for this task to be completed, but rather start other tasks meanwhile. Since the "other" tasks don't wait for the original task to be completed either, there is no way to ensure that the tasks stay in sync without writing synchronous code. When you start multiple async tasks, you give up control of exactly when those tasks are executed to the OS and its processor scheduling algorithm, with the promise that they will be done eventually.

The scheduling algorithm will choose the most efficient way of executing your code, taking into consideration every other program that is asking for CPU time, and decide how to run your tasks. It can choose to run one task to completion, and then the other task. Or, it can choose to run them on different processor cores, or switch between them on the same core. This is not up to you, but rather up to the OS to decide.

Specifically, in C#, async tasks are run using a thread pool so you can't even choose whether or not to run multiple threads. If the tasks are run using the same thread, they can't run on multiple cores , which means that they can't run in parallel. If you want tasks to run on multiple cores, you need to use Thread s explicity, but regardless you can't control when each thread runs or which core it runs on.

If you have long-running tasks, you may see that your output switches from one to the other. The exact amount of time that each task runs for depends on many things that you can't control, and will possibly be different each time you run the program.

If you need your code to run synchronously (ie, waiting for some other task to continue before running a task), you need to write synchronous code, not async.

You do two things sequentially:

(1) start task 1. 
(2) start task 2.

Now starting a task, ie executing Task.Run() , is slow and expensive. It takes perhaps, say, 5ms. Running that very short task takes perhaps only 1ms. So task 1 is finished long before task 2 has started.

Time ->                 ...1ms...1ms...1ms...1ms...1ms...1ms..........1ms............1ms...1ms...1ms...1ms...1ms...
main [set up task 1..................................][set up task 2............................................][main ends]
task1                                                 [DoWork() starts] [DoWork() ends]
task2                                                                                                            [DoWorkToo() starts] [DoWorkToo() ends]

If you want to have the tasks run in parallel you must make their runtime long in comparison to the task startup time. One solution is simply to run the loop many thousands of times, but that can be unwieldy. Better is to let the tasks sleep during loop execution so that task1 is still running when task 2 starts.

Here is an example with longer running tasks showing nicely how the execution interleaves:

using System;
using System.Threading;
using System.Threading.Tasks;

using System.Diagnostics; // stopwatch

namespace TwoTasks2
{
    class Program
    {
        static Stopwatch globalStopWatch = new Stopwatch();

        private static void DoWork()
        {
            Console.WriteLine("========================= Entering DoWork() after " + globalStopWatch.ElapsedMilliseconds);
            int sum = 0;
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                if (i % 10000 == 0) 
                { 
                    Console.WriteLine("Task one " + 10000 + " cycles took " + sw.ElapsedMilliseconds + " ms");
                    sw.Stop();
                    sw.Start();
                    Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);
                    Console.WriteLine("CPU ID:    " + Thread.GetCurrentProcessorId());
                }
            }
        }

        private static void DoWorkToo()
        {
            Console.WriteLine("========================= Entering DoWorkToo() after " + globalStopWatch.ElapsedMilliseconds);
            int sum = 0;
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                if (i % 10000 == 0) 
                {
                    Console.WriteLine("                                  Task two " + 10000 + " cycles took " + sw.ElapsedMilliseconds + " ms");
                    sw.Stop();
                    sw.Start();
                    Console.WriteLine("                                  Thread ID: " + Thread.CurrentThread.ManagedThreadId);
                    Console.WriteLine("                                  CPU ID:    " + Thread.GetCurrentProcessorId());
                    
                }
            }
        }

        public static void Main()
        {
            globalStopWatch.Start();
            var task1 = Task.Run(() => DoWork());
            long ms = globalStopWatch.ElapsedMilliseconds;
            Console.WriteLine("--------------------- RunTask 1 took " + ms);

            var task2 = Task.Run(() => DoWorkToo());
            Console.WriteLine("--------------------- RunTask 2 took " + (globalStopWatch.ElapsedMilliseconds-ms));

            var tasks = new Task[] { task1, task2 };
            Task.WaitAll(tasks);
        }
    }
}


Example output on my machine, Debug build:

--------------------- RunTask 1 took 23
========================= Entering DoWork() after 39
--------------------- RunTask 2 took 18
Task one 10000 cycles took 0 ms
Thread ID: 4
========================= Entering DoWorkToo() after 41
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    1
CPU ID:    2
                                  Task two 10000 cycles took 1 ms
                                  Thread ID: 5
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    4
                                  CPU ID:    1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    4
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    4
                                  CPU ID:    1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    4
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    2
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID:    2
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID:    2
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID:    2
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    3
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID:    4
                                  Task two 10000 cycles took 2 ms
                                  Thread ID: 5
                                  CPU ID:    10
                                  Task two 10000 cycles took 3 ms
                                  Thread ID: 5
                                  CPU ID:    1

A Release build is much orderlier:

--------------------- RunTask 1 took 21
========================= Entering DoWork() after 37
--------------------- RunTask 2 took 16
Task one 10000 cycles took 0 ms
Thread ID: 4
CPU ID:    4
Task one 10000 cycles took 0 ms
Thread ID: 4
CPU ID:    6
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID:    2
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID:    2
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID:    3
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID:    6
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    2
========================= Entering DoWorkToo() after 39
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    10
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    10
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID:    10
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    1
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    1
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    11
                                  Task two 10000 cycles took 0 ms
                                  Thread ID: 5
                                  CPU ID:    1
                                  Task two 10000 cycles took 1 ms
                                  Thread ID: 5
                                  CPU ID:    1

It takes a whopping 35ms until the first task is running! That's eternity on modern CPUs. The second task then starts much faster.

The tasks take turns even between printing lines to the console. You can also see that even the same thread is hopping from core to core as Windows sees fit. (That actually surprised me. My Ryzen has 6 real cores, and there isn't any significant load, so I'd leave the tasks running where they are.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM