技术频道导航
HTML/CSS
.NET技术
IIS技术
PHP技术
Js/JQuery
Photoshop
Fireworks
服务器技术
操作系统
网站运营

赞助商

分类目录

赞助商

最新文章

搜索

如何在C#中使用Parallel.For和Parallel.ForEach

作者:admin    时间:2023-6-5 17:29:11    浏览:

并行性是在具有多个内核的系统上并行执行任务的能力。.NET Framework 4 中引入了对 .NET 中并行编程的支持。.NET 中的并行编程使我们能够更有效地使用系统资源,并通过更好的编程控制。本文讨论了我们如何在 .NET Core 应用程序中使用并行性。

要使用本文中提供的代码示例,你应该在系统中安装 Visual Studio 2019。

如何在C#中使用Parallel.For和Parallel.ForEach

在 Visual Studio 中创建 .NET Core 控制台应用程序项目

首先,让我们在 Visual Studio 中创建一个 .NET Core 控制台应用程序项目。假设你的系统中安装了 Visual Studio 2019,请按照下面概述的步骤在 Visual Studio 中创建一个新的 .NET Core 控制台应用程序项目。

  1. 启动 Visual Studio IDE。
  2. 单击“创建新项目”。
  3. 在“创建新项目”窗口中,从显示的模板列表中选择“控制台应用程序(.NET Core)”。
  4. 点击下一步。
  5. 在“配置新项目”窗口中,指定新项目的名称和位置。
  6. 单击创建。

在本文的后续部分中,我们将使用该项目来说明 .NET Core 中的并行编程。

.NET Core 中的并发性和并行性

并发性和并行性是 .NET 和 .NET Core 中的两个关键概念。尽管它们看起来相同,但它们之间存在细微差别。

考虑必须由应用程序执行的两个任务 T1 和 T2。如果一个任务处于执行状态而另一个任务等待轮到它,则这两个任务处于并发执行状态。结果,其中一项任务先于另一项完成。相比之下,如果两个任务同时执行,则这两个任务是并行执行的。要实现任务并行,程序必须运行在多核 CPU 上。

.NET Core 中的 Parallel.For 和 Parallel.ForEach

Parallel.For 循环执行可以并行运行的迭代。你可以监视甚至操纵循环的状态。Parallel.For 循环就像 for 循环,只是它允许迭代在多个线程中并行运行。

Parallel.ForEach 方法将要完成的工作拆分为多个任务,每个任务对应集合中的每一项。Parallel.ForEach 类似于 C# 中的 foreach 循环,除了 foreach 循环在单个线程上运行并且按顺序进行处理,而 Parallel.ForEach 循环在多个线程上运行并且处理以并行方式进行。

C# 中的 Parallel.ForEach 与 foreach

考虑下面的方法,它接受一个整数作为参数,如果它是质数则返回 true

static bool IsPrime(int integer)
{
    if (integer <= 1) return false;
    if (integer == 2) return true;
    var limit = Math.Ceiling(Math.Sqrt(integer));
    for (int i = 2; i <= limit; ++i)
        if (integer % i == 0)
            return false;
    return true;
}

我们现在将利用 ConcurrentDictionary 来存储素数和托管线程 ID。由于两个范围之间的质数是唯一的,我们可以将它们用作键,将托管线程 ID 用作值。

.NET 中的并发集合包含在 System.Collections.Concurrent 命名空间内,并提供集合类的无锁和线程安全实现。ConcurrentDictionary 类包含在 System.Collections.Concurrent 命名空间内,代表一个线程安全的字典。

以下两个方法均使用 IsPrime 方法来检查整数是否为素数,将素数和托管线程 ID 存储在 ConcurrentDictionary 的实例中,然后返回该实例。第一种方法使用并发,第二种方法使用并行。

private static ConcurrentDictionary<int, int>
GetPrimeNumbersConcurrent(IList<int> numbers)
{
    var primes = new ConcurrentDictionary<int, int>();
    foreach (var number in numbers)
    {               
        if(IsPrime(number))
        {
            primes.TryAdd(number,
            Thread.CurrentThread.ManagedThreadId);
         }
     }
     return primes;
}
private static ConcurrentDictionary<int, int>
GetPrimeNumbersParallel(IList<int> numbers)
{
    var primes = new ConcurrentDictionary<int, int>();
    Parallel.ForEach(numbers, number =>
    {
        if (IsPrime(number))
        {
            primes.TryAdd(number,
            Thread.CurrentThread.ManagedThreadId);
        }
    });
    return primes;
}

C# 中的并发与并行示例

以下代码片段说明了如何调用 GetPrimeNumbersConcurrent 方法来检索 1 到 100 之间的所有质数以及托管线程 ID。

static void Main(string[] args)
{
    var numbers = Enumerable.Range(0, 100).ToList();
    var result = GetPrimeNumbersConcurrent(numbers);
    foreach(var number in result)
    {
        Console.WriteLine($"Prime Number:
        {string.Format("{0:0000}",number.Key)},
        Managed Thread Id: {number.Value}");
    }
    Console.Read();
}

当你执行上面的程序时,你应该看到如图 1 所示的输出:

C#调用GetPrimeNumbersConcurrent方法来检索1到100之间的所有质数 
图1

如你所见,托管线程 ID 在每种情况下都是相同的,因为我们在此示例中使用了并发。现在让我们看看使用线程并行时输出会是什么样子。以下代码片段说明了如何使用并行性检索 1 到 100 之间的素数。

static void Main(string[] args)
{
    var numbers = Enumerable.Range(0, 100).ToList();
    var result = GetPrimeNumbersParallel(numbers);
    foreach(var number in result)
    {
        Console.WriteLine($"Prime Number:
        {string.Format("{0:0000}",number.Key)},
        Managed Thread Id: {number.Value}");
    }
    Console.Read();
}

当你执行上面的程序时,输出应该类似于图 2 所示: 

C#使用并行性检索 1 到 100 之间的素数 
图2

正如你在这里看到的,因为我们使用了 Parallel.ForEach,所以创建了多个线程,因此托管线程 ID 是不同的。

限制C#中的并行度

并行度是一个无符号整数,表示你的查询在执行时应利用的最大处理器数。换句话说,并行度是一个整数,表示将在同一时间点执行以处理查询的最大任务数。

默认情况下,Parallel.ForParallel.ForEach 方法对衍生任务的数量没有限制。因此,在上面显示的 GetPrimeNumbersParallel 方法中,程序尝试使用系统中的所有可用线程。

你可以利用 MaxDegreeOfParallelism 属性来限制衍生任务的数量(每个 Parallel 类的 ParallelOptions 实例)。如果 MaxDegreeOfParallelism 设置为 -1,则并发运行的任务数没有限制。

以下代码片段显示了如何设置 MaxDegreeOfParallelism 以使用最多 75% 的系统资源。 

new ParallelOptions
{
    MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.75) * 2.0))
};

请注意,在上面的代码片段中,我们将处理器数量乘以二,因为每个处理器包含两个内核。以下是 GetPrimeNumbersParallel 方法的完整更新代码,供你参考:

private static ConcurrentDictionary<int, int> GetPrimeNumbersParallel(IList<int> numbers)
{
    var primes = new ConcurrentDictionary<int, int>();
    Parallel.ForEach(numbers, number =>
    {
        new ParallelOptions
        {
            MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.75) * 2.0))
        };
        if (IsPrime(number))
        {
            primes.TryAdd(number,
            Thread.CurrentThread.ManagedThreadId);
        }
    });
    return primes;
}

确定 C# 中的并行循环是否完成

请注意,Parallel.ForParallel.ForEach 都返回一个 ParallelLoopResult 实例,该实例可用于确定并行循环是否已完成执行。以下代码片段显示了如何使用 ParallelLoopResult

ParallelLoopResult parallelLoopResult = Parallel.ForEach(numbers, number =>
 {
    new ParallelOptions
    {
          MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling(
          (Environment.ProcessorCount * 0.75) * 2.0))
    };
    if (IsPrime(number))
    {
          primes.TryAdd(number, Thread.CurrentThread.ManagedThreadId);
    }
 });
Console.WriteLine("IsCompleted: {0}", parallelLoopResult.IsCompleted);

要在非泛型集合中使用 Parallel.ForEach,你应该利用 Enumerable.Cast 扩展方法将集合转换为泛型集合,如下面的代码片段所示。

Parallel.ForEach(nonGenericCollection.Cast<object>(),
currentElement =>
{
});

最后一点,不要假设 Parallel.ForParallel.ForEach 的迭代将始终并行执行。你还应该注意线程亲和性问题。你可以阅读有关任务并行性的潜在缺陷。

总结

本文讨论了如何在C#中使用Parallel.ForParallel.ForEach的问题。通过本文的学习,你应该了解了C#中Parallel.ForParallel.ForEach的使用方法及注意问题。

相关文章

x
  • 站长推荐
/* 左侧显示文章内容目录 */