簡體   English   中英

如何使用 C# 並行運行兩個任務?

[英]How to run two tasks in parallel with C#?

假設我有這段代碼應該正確並行運行兩個任務,我對 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);
     }
 }

我得到的結果:

 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
  

我想顯示這樣的結果:

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

我怎樣才能達到這個結果?

異步代碼的想法是告訴處理器不要等待這個任務完成,而是同時啟動其他任務。 由於“其他”任務也不會等待原始任務完成,因此如果不編寫同步代碼,就無法確保任務保持同步。 當您啟動多個異步任務時,您放棄了對這些任務何時執行的控制權及其處理器調度算法,並承諾它們最終會完成。

調度算法將選擇執行代碼的最有效方式,考慮到所有其他要求 CPU 時間的程序,並決定如何運行您的任務。 它可以選擇運行一個任務來完成,然后另一個任務。 或者,它可以選擇在不同的處理器內核上運行它們,或者在同一內核上切換它們。 這不取決於您,而是取決於操作系統來決定。

具體來說,在 C# 中,異步任務使用線程池運行,因此您甚至無法選擇是否運行多個線程。 如果任務使用同一個線程運行,它們就不能在多個核心上運行,這意味着它們不能並行運行。 如果您希望任務在多個內核上運行,則需要顯式使用Thread ,但無論您無法控制每個線程何時運行或運行在哪個內核上。

如果您有長時間運行的任務,您可能會看到輸出從一個切換到另一個。 每個任務運行的確切時間取決於許多您無法控制的事情,並且每次運行程序時可能會有所不同。

如果您需要您的代碼同步運行(即,在運行任務之前等待其他任務繼續執行),您需要編寫同步代碼,而不是異步代碼。

依次做兩件事:

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

現在開始一個任務,即執行Task.Run() ,既慢又昂貴。 比如說,它可能需要 5 毫秒。 運行這個非常短的任務可能只需要 1 毫秒。 所以任務 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]

如果您想讓任務並行運行,則必須使它們的運行時間長於任務啟動時間。 一種解決方案是簡單地運行循環數千次,但這可能很笨拙。 更好的是讓任務在循環執行期間休眠,以便在任務 2 啟動時任務 1 仍在運行。

這是一個運行時間較長的任務的示例,很好地展示了執行交錯的方式:

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


我的機器上的示例輸出,調試構建:

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

發布版本更加有序:

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

第一個任務運行需要 35 毫秒! 這是現代 CPU 的永恆。 然后第二個任務開始得更快。

這些任務甚至在打印到控制台的行之間輪流執行。 您還可以看到,即使是相同的線程也在 Windows 認為合適的情況下從一個核心跳到另一個核心。 (這實際上讓我感到驚訝。我的 Ryzen 有 6 個真正的內核,並且沒有任何顯着的負載,所以我會讓任務在它們所在的位置運行。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM