.NET 6 新特性 Parallel ForEachAsync

.NET 6 新特性 Parallel ForEachAsync

Intro

在 .NET 6 中有一个 API Parallel.ForEachAsync 在官方的博客中一直被忽略,但是我觉得这个 API 非常的实用,类似于同步版本的 Parallel.ForEach,可以比较高效地控制多个异步任务的并行度。之前的版本中我们会使用信号量来控制异步任务的并发度,使用这个 API 之后就可以大大简化我们的代码,详细可以看下面的示例代码。

为什么需要这个 API

API definition

在使用同步任务并行执行的时候, 我们可以使用 Parallel.ForEach 来比较方便的控制多个任务的并行度,以便更好的利用系统资源,比如任务中如果有对受限的系统资源进行访问的时候,此时最好就能够控制并行度, 避免系统资源争用,效率反而不高。

Parallel.ForEachAsync 相关的 API 定义如下:

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource, CancellationToken, ValueTask> body);

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IEnumerable<TSource> source, CancellationToken cancellationToken, System.Func<TSource, CancellationToken, ValueTask> body);

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IEnumerable<TSource> source, System.Threading.Tasks.ParallelOptions parallelOptions, System.Func<TSource, CancellationToken, ValueTask> body);

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IAsyncEnumerable<TSource> source, System.Func<TSource, CancellationToken, ValueTask> body);

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, System.Func<TSource, CancellationToken, ValueTask> body);

public static System.Threading.Tasks.Task ForEachAsync<TSource>(System.Collections.Generic.IAsyncEnumerable<TSource> source, System.Threading.Tasks.ParallelOptions parallelOptions, System.Func<TSource, CancellationToken, ValueTask> body);

通过 ParallelOptions 我们可以限制最大并行度以及自定义 TaskScheduler 和取消令牌

public class ParallelOptions
{

    private TaskScheduler _scheduler;
    private int _maxDegreeOfParallelism;
    private CancellationToken _cancellationToken;

    public ParallelOptions()
    {
        this._scheduler = TaskScheduler.Default;
        this._maxDegreeOfParallelism = -1;
        this._cancellationToken = CancellationToken.None;
    }


    public TaskScheduler? TaskScheduler
    {
        get => this._scheduler;
        set => this._scheduler = value;
    }

    public int MaxDegreeOfParallelism
    {
        get => this._maxDegreeOfParallelism;
        set => this._maxDegreeOfParallelism = value != 0 && value >= -1 ? value : throw new ArgumentOutOfRangeException(nameof (MaxDegreeOfParallelism));
    }

    public CancellationToken CancellationToken
    {
        get => this._cancellationToken;
        set => this._cancellationToken = value;
    }
}

Sample

来看一个实际的示例吧,多个任务并行执行,通常我们会使用 Task.WhenAll 来并行多个 Task 的执行,但是 Task.WhenAll 不能限制并发度,通常我们是会在异步 task 上封装一层,使用信号量来限制并发,示例如下:

using var semaphore = new SemaphoreSlim(10, 10);
await Task.WhenAll(Enumerable.Range(1, 100).Select(async _ =>
{
    try
    {
        await semaphore.WaitAsync();
        await Task.Delay(1000);
    }
    finally
    {
        semaphore.Release();
    }
}));

使用 Parallel.ForEachAsync 之后,我们就可以大大简化我们的代码:

await Parallel.ForEachAsync(Enumerable.Range(1, 100), new ParallelOptions()
{
    MaxDegreeOfParallelism = 10
}, async (_, _) => await Task.Delay(1000));

这样是不是简单了很多。

再来看一个所有情况的对比,来看一下是不是符合我们的预期:

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

var watch = Stopwatch.StartNew();
await Task.WhenAll(Enumerable.Range(1, 100).Select(_ => Task.Delay(1000)));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

watch.Restart();
using var semaphore = new SemaphoreSlim(10, 10);
await Task.WhenAll(Enumerable.Range(1, 100).Select(async _ =>
{
    try
    {
        await semaphore.WaitAsync();
        await Task.Delay(1000);
    }
    finally
    {
        semaphore.Release();
    }
}));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");

watch.Restart();
await Parallel.ForEachAsync(Enumerable.Range(1, 100), async (_, _) => await Task.Delay(1000));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

watch.Restart();
await Parallel.ForEachAsync(Enumerable.Range(1, 100), new ParallelOptions()
{
    MaxDegreeOfParallelism = 10
}, async (_, _) => await Task.Delay(1000));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

watch.Restart();
await Parallel.ForEachAsync(Enumerable.Range(1, 100), new ParallelOptions()
{
    MaxDegreeOfParallelism = 100
}, async (_, _) => await Task.Delay(1000));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

watch.Restart();
await Parallel.ForEachAsync(Enumerable.Range(1, 100), new ParallelOptions()
{
    MaxDegreeOfParallelism = int.MaxValue
}, async (_, _) => await Task.Delay(1000));
watch.Stop();
WriteLine(watch.ElapsedMilliseconds);

可以先想一下,每种方式执行需要的耗时大概是多久,之后再尝试运行一下看一下结果

输出结果如下:

output

More

执行结果是不是符合你的预期呢?

默认情况下,Parallel.ForEachAsync 的最大并行度是当前机器的 CPU 数量,也就是 Environment.ProcessorCount,如果要不限制可以指定最大并行度为 int.MaxValue

References

  • https://github.com/WeihanLi/SamplesInPractice/tree/master/net6sample/ParallelSample

  • https://github.com/dotnet/runtime/pull/46943

  • https://github.com/dotnet/runtime/blob/911640b3a891f92ff66e9c82ce65f71d203f11a2/src/libraries/System.Threading.Tasks.Parallel/ref/System.Threading.Tasks.Parallel.cs#L39-L44

很抱歉,目前JDK的最新版本是JDK 17,而不是JDK 21。以下是JDK 8到JDK 17的一些新特性介绍: JDK 8新特性: 1. Lambda表达式:引入了函数式编程的概念,简化了代码编写。 2. Stream API:提供了一种更便捷的方式来处理集合数据。 3. 接口的默认方法:允许在接口中定义默认的方法实现。 4. 方法引用:可以通过方法的名字来引用方法,简化了代码编写。 5. 新的日期和时间API:提供了更好的日期和时间处理方式。 JDK 9新特性: 1. 模块化系统:引入了Java平台模块系统(JPMS),提供了更好的代码组织和隔离性。 2. JShell:交互式编程工具,可以在命令行中进行Java代码的实时执行和测试。 3. 私有接口方法:允许在接口中定义私有方法。 4. 改进的Stream API:引入了一些新的操作方法,提供了更好的性能和灵活性。 JDK 10新特性: 1. 局部变量类型推断:可以使用var关键字来声明局部变量,编译器会自动推断变量类型。 2. 应用类数据共享(Application Class-Data Sharing):可以在多个Java进程之间共享类数据,提高启动时间和内存利用率。 3. 并行全垃圾回收器(Parallel Full GC):提供了一种新的垃圾回收器,可以在全局垃圾回收时并行处理。 JDK 11新特性: 1. HTTP客户端API:引入了一套新的标准HTTP客户端API,替代了老旧的HttpURLConnection。 2. 嵌套访问控制:允许在类的内部定义嵌套的私有接口和私有类,并对外部类进行访问控制。 3. Epsilon垃圾回收器:一种实验性的垃圾回收器,用于性能测试和短期任务。 JDK 12到JDK 17的新特性包括了更多的改进和优化,如增强的Switch语句、新的垃圾回收器、增强的Pattern匹配等。如果你对其中的某个版本特性有具体的问题,我可以为你提供更详细的解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值