C# 中的多线程(Parallel.For(),线程类或异步和等待)

本文关键字:异步 等待 线程 多线程 Parallel For | 更新日期: 2023-09-27 18:31:02

简介

只是出于好奇,我怎样才能制作一个多线程循环,其中订单从低到高保证,并且在循环完成后执行了Console.WriteLine("Done");。我已经尝试了下面解释的三种情况。

<小时 />

案例一:Parallel.For(...)

如果要创建一个在 C# 中并行运行的 for 循环,则可以使用以下代码:

ParallelLoopResult result = Parallel.For(0, 10, i => 
{
    Console.WriteLine($"{i}, task: {Task.CurrentId}, Thread: {Thread.CurrentThread.ManagedThreadId}";
    Thread.Sleep(10);
}
Console.WriteLine($"Is completed: {result.IsCompleted}";

这将给出无法保证订单的下一个输出。如果再次运行代码,您将看到不同的结果。程序的运行顺序为 0-2-4-6-8-...有五个任务和六个线程。

0, task: 1, thread: 1
2, task: 2, thread: 3
4, task: 3, thread: 4
6, task: 4, thread: 5
8, task: 5, thread: 6
5, task: 3, thread: 4
7, task: 4, thread: 5
9, task: 5, thread: 6
3, task: 2, thread: 3
1, task: 1, thread: 1
Is completed: True
<小时 />

案例二:Thread

我还尝试使用线程而不是Parallel类。这是代码:

public static void Main(string[] args)
{
    for (int i = 0; i < number; i++)
    {
        Thread tr = new Thread(() => Print(i));
        tr.IsBackground = true;
        tr.Start();
    }
    Console.WriteLine("Done");
}
private static void Print(int i)
{
    Console.WriteLine(i);
    Thread.Sleep(10);
}

此代码的以下结果是,现在不能保证从低到高的顺序,并且字符串Done是在 for 循环结束之后编写的,但并不总是保证,但是我在 Console.WriteLine("Done"); 之后放置了thread.Start();

1
2
3
5
7
7
8
9
9
Done

就像我第二次跑步一样:

2
3
3
4
5
6
7
8
Done
10
<小时 />

案例树:asyncawait

我也会尝试这种方式,但我不知道如何实现这种循环线程方式。

<小时 />

问题

在所有情况下,我都没有我需要的结果。在第一种情况下,它不能保证被订购,而在第二种情况下,Done循环运行之前写入。所以我的问题是我如何运行一个 for 循环,其中订单始终得到保证并在循环运行后Done写入?

C# 中的多线程(Parallel.For(),线程类或异步和等待)

你问的是如何创建一个多线程循环,保证执行顺序。 如前所述,你不能。 但是,如果您想订购输出,则可以这样做。在这里你有两个选择

  1. 保存结果,直到它们全部完成(请参阅下面的伪代码):

    • 创建输出输入字典
    • 创建一个多线程循环来处理所有输入并记录输出
    • 完成所有操作后,将输出(按输入顺序)写入屏幕
  2. 完成的输出进行排序。 这比#1更困难,但可以让您更快地显示结果。

    • 创建一个多线程循环来处理所有输入
    • 每个线程完成后,显示其输出,但不要只是将其写入屏幕,而是将其插入所有排序输出中的适当位置。

尽管您不应该将线程用于此类顺序用途。你所要求的是可能的。

这是我想要实现的目标的例子。它使用多个线程对象并保证顺序。但是,按顺序执行可能会获得更好的性能。

    public static void PrintDone()
    {
        Console.WriteLine("Done");
    }
    public static void Print(int i)
    {
        Console.WriteLine(i);
        Thread.Sleep(10);
    }
    static void Main(string[] args)
    {
        List<Thread> threads = new List<Thread>();
        for(int i = 0; i < 10; ++i)
        {
            int numCopy = i;
            Thread th = new Thread(() => Print(numCopy));
            threads.Add(th);
        }
        for (int i = 0; i < 10; ++i)
        {
            threads[i].Start();
            threads[i].Join();
        }
        PrintDone();
        Console.ReadLine();
    }

所以要点是创建一堆线程,然后按顺序启动它们(实际上没有办法绕过那部分),然后直接加入主线程。将发生的情况是主线程将启动一个"子线程",然后等待它完成,一旦完成,它将启动下一个线程,并重复直到它不在列表中的线程中。

但是,如果您试图并行打印数字并保证输出的顺序,那么您就不走运了。仍然有可能,但是尝试维护顺序将获得的开销将破坏并行化的目的。

这是代码的真正并行版本:

    public static void PrintDone()
    {
        Console.WriteLine("Done");
    }
    public static void Print(int i, int[] threadStatus)
    {
        if(i != 0)
        {
            while(threadStatus[i-1] < 0)
            {
                Thread.Sleep(10);
            }
        }
        Thread.Sleep(100);
        threadStatus[i] = i;
        Console.WriteLine(i);
    }
    static void Main(string[] args)
    {
        int[] threadStatus = Enumerable.Repeat(-1, 10).ToArray();
        List<Thread> threads = new List<Thread>();
        for(int i = 0; i < 10; ++i)
        {
            int numCopy = i;
            Thread th = new Thread(() => Print(numCopy, threadStatus));
            threads.Add(th);
        }
        for (int i = 0; i < 10; ++i)
        {
            threads[i].Start();
        }
        threads[9].Join();
        PrintDone();
        Console.ReadLine();
    }

由于每个线程只能写入 int 数组的一部分,并且读取是原子的,因此我们可以使用 int 数组来保持当前正在运行的所有线程的状态。

这基本上意味着在任何特定的"打印"线程中,您可以检查它们是否准备好在线程内打印,如果没有,您可以告诉它们循环睡眠,以便它们定期检查其状态。