简体   繁体   English

如何使用 C# 并行运行两个任务?

[英]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 )假设我有这段代码应该正确并行运行两个任务,我对 C# 中的并行任务没有一个好主意,我想从这段代码开始理解这个概念,我想做同时运行两个任务(异步)

 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.调度算法将选择执行代码的最有效方式,考虑到所有其他要求 CPU 时间的程序,并决定如何运行您的任务。 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.具体来说,在 C# 中,异步任务使用线程池运行,因此您甚至无法选择是否运行多个线程。 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.如果您希望任务在多个内核上运行,则需要显式使用Thread ,但无论您无法控制每个线程何时运行或运行在哪个内核上。

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.现在开始一个任务,即执行Task.Run() ,既慢又昂贵。 It takes perhaps, say, 5ms.比如说,它可能需要 5 毫秒。 Running that very short task takes perhaps only 1ms.运行这个非常短的任务可能只需要 1 毫秒。 So task 1 is finished long before task 2 has started.所以任务 1 早在任务 2 开始之前就完成了。

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.更好的是让任务在循环执行期间休眠,以便在任务 2 启动时任务 1 仍在运行。

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!第一个任务运行需要 35 毫秒! That's eternity on modern CPUs.这是现代 CPU 的永恒。 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.您还可以看到,即使是相同的线程也在 Windows 认为合适的情况下从一个核心跳到另一个核心。 (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.) (这实际上让我感到惊讶。我的 Ryzen 有 6 个真正的内核,并且没有任何显着的负载,所以我会让任务在它们所在的位置运行。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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