首页 > 其他分享 >【转载】异步 LINQ

【转载】异步 LINQ

时间:2024-04-22 10:35:34浏览次数:34  
标签:异步 IAsyncEnumerable await LINQ item 转载 Where

原文:https://blog.csdn.net/zls365365/article/details/124395742

LINQ 这个东西,出来很早了,写过几年代码的兄弟们,或多或少都用过一些。

早期的 LINQ,主要是同步的,直到 C# 8.0 加入 IAsyncEnumerable,LINQ 才真正转向异步。这本来是个非常好的改变,配合 System.Linq.Async 库提供的扩展,可以在诸如 Where、Select、GroupBy 等各种地方用到异步。

但事实上,在我 Review 代码时,见了很多人的代码,并没有按异步的规则去使用,出现了很多的坑。

举个简单的例子:+

static async Task<List<T>> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{
    var filteredItems = new List<T>();
    await foreach (var item in source)
    {
        if (predicate(item))
        {
            filteredItems.Add(item);
        }
    }
 
    return filteredItems;
}

这样的写法,看着是用到了 async / await 对,但实际上并没有实现异步,程序依然是按照同步在运行。换句话说,这只是一个样子上的异步,实际没有任何延迟执行的效果。

1. 延迟执行

其实,这儿正确的写法也挺简单,用到的就是个异步的迭代器(关于异步迭代器,如果需要了解,可以看我的另一篇推文):

static async IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{
    await foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

这种写法下,编译器会将方法转了状态机,并在实际调用时,才通过枚举器返回异步枚举项。

看看调用过程:

IAsyncEnumerable<User> users = ...
IAsyncEnumerable<User> filteredUsers = users.Where(User => User.Name == "WangPlus");
 
await foreach (User user in filteredUsers)
{
    Console.WriteLine(user.Age);
}

在这个调用的例子中,在 Where 时,实际方法并不会马上开始。只有在下面 foreach 时,才真正开始执行 Where 方法。

延迟执行,这是异步 LINQ 的第一个优势。

2. 流执行

流执行,依托的也是异步迭代器。

所谓流执行,其实就是根据调用的要求,一次返回一个对象。通过使用异步迭代器,可以不用一次返回所有的对象,而是一个一个地返回单个的对象,直到枚举完所有的对象。

流执行需要做个技巧性的代码,需要用到一个 C# 8.0 的新特性:局部方法。

看代码:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{
    return Core();
 
    async IAsyncEnumerable<T> Core()
    {
        await foreach (var item in source)
        {
            if (predicate(item))
            {
                yield return item;
            }
        }
    }
}

3. 取消异步 LINQ

前面两个小节,写的是异步 LINQ 的执行。

通常使用异步 LINQ 的原因,就是因为执行时间长,一般需要一段时间来完成。因此,取消异步 LINQ 就很重要。想象一下,一个长的 DB 查询已经超时了的情况,该怎么处理?

为了支持取消,IAsyncEnumerable.GetEnumerator 本身接受一个 CancellationToken 参数来中止任务,并用一个扩展方法挂接到 foreach 调用:

CancellationToken cancellationToken = ...
IAsyncEnumerable<User> users = ...
IAsyncEnumerable<User> filteredUsers = users.Where(User => User.Name == "WangPlus");
 
await foreach (var User in filteredUsers.WithCancellation(cancellationToken))
{
    Console.WriteLine(User.Age);
}

同时,在上面的 Where 定义中,也要响应 CancellationToken 参数:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{
    return Core();
 
    async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in source.WithCancellation(cancellationToken))
        {
            if (predicate(item))
            {
                yield return item;
            }
        }
    }
}

多解释一下:在 Where 方法中,CancellationToken 只能加到局部函数 Core 中,一个简单的原因是 Where 本身并不是异步方法,而且,我们也不希望从 Where 往里传递。想象一下:

Users.Where(xxx, cancellationToken).Select(xxx, cancellationToken).OrderBy(xxx, cancellationToken);

这样的代码会让人晕死。

所以,我们会采用上面的方式,允许消费者在枚举数据时传递 CancellationToken 来达到取消异步操作的目的。
4. 处理ConfigureAwait(false)

这是另一个异步必须要注意的部分,其实就是上下文。

通常大多数的方法,我们不需要关注上下文,但总有一些需要,在等待的异步操作恢复后,需要返回到某个上下文的情况。这种情况在 UI 线程编码时通常都需要考虑。很多人提到的异步死锁,就是这个原因。

处理也很简单:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{
    return Core();
 
    async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
        {
            if (predicate(item))
            {
                yield return item;
            }
        }
    }
}

这儿也多说两句:按微软的说法,await foreach 本身是基于模式的,WithCancellation 和 ConfigureAwait 返回同样的结构体 ConfiguredCancelableAsyncEnumerable。这个结构体没有实现 IAsyncEnumerable 接口,而是做了一个 GetAsyncEnumerator 方法,返回一个具有 MoveNextAsync、Current、DisposeAsync 的枚举器,因此可以 await foreach 。

5. 方法扩展

上面 4 个小节,我们完成了一个 Where 异步 LINQ 的全部内容。

不过,这个方法有一些限制和不足。熟悉异步的兄弟们应该已经看出来了,里面用了一个委托 predicate 来做数据过滤,而这个委托,是个同步的方法。

事实上,根据微软对异步 LINQ 的约定,每个操作符应该是三种重载:

  • 同步委托的实现,就是上面的 Where 方法;

  • 异步委托的实现,这个是指具有异步返回类型的实现,通常这种方法名称会用一个 Await 做后缀,例如:WhereAwait;

  • 可以接受取消的异步委托的实现,通常这种方法会用 AwaitWithCancellation 做后缀,例如:WhereAwaitWithCancellation。

参考微软的异步方法,基本上都是以这种结构来命名方法名称的。

下面,我们也按这个方式,来做一个 Where 方法的几个重载。

WhereAwait 方法

上面说了,这会是一个异步实现。所以,条件部分就不能用 Func<T, bool> 这样的同步委托了,而需要改为 Func<T, ValueTask<bool>>。这里的 ValueTask 倒不是必须,用 Task 也可以,只不过我更习惯用 ValueTask。两个的区别:Task 是类,有上下文,而 ValueTask 是结构。

代码是这样:

static IAsyncEnumerable<T> WhereAwait<T>(this IAsyncEnumerable<T> source, Func<T, ValueTask<bool>> predicate)
{
    return Core();
 
    async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
        {
            if (await predicate(item).ConfigureAwait(false))
            {
                yield return item;
            }
        }
    }
}

调用时是这样:

IAsyncEnumerable<User> filteredUsers = users.WhereAwait(async user => await someIfFunction());

WhereAwaitWithCancellation方法

在上面的基础上,又加了一个取消操作。

看代码:

static IAsyncEnumerable<T> WhereAwaitWithCancellation<T>(this IAsyncEnumerable<T> source, Func<T, CancellationToken, ValueTask<bool>> predicate)
{
    return Core();
 
    async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
        {
            if (await predicate(item, cancellationToken).ConfigureAwait(false))
            {
                yield return item;
            }
        }
    }
}

调用时是这样:

IAsyncEnumerable<User> filteredUsers = users.WhereAwaitWithCancellation(async (user, token) => await someIfFunction(user, token));

6. 总结

异步 LINQ,多数是在 LINQ 的扩展方法中使用,而不是我们通常习惯的 LINQ 直写。

事实上,异步 LINQ 的扩展,对 LINQ 本身是有比较大的强化作用的,不管从性能,还是可读性上,用多了,只会更爽。

标签:异步,IAsyncEnumerable,await,LINQ,item,转载,Where
From: https://www.cnblogs.com/cdaniu/p/18150149

相关文章

  • Web【转载学习】
    Web[转载学习]随着WEB2.0、社交网络、微博等等一系列新型的互联网产品的诞生,基于WEB环境的互联网应用越来越广泛,企业信息化的过程中各种应用都架设在WEB平台上,WEB业务的迅速发展也引起黑客们的强烈关注,接踵而至的就是WEB安全威胁的凸显,黑客利用网站操作系统的漏洞和WE......
  • 如何基于Django中的WebSockets和异步视图来实现实时通信功能
    本文分享自华为云社区《结合Django中的WebSockets和异步视图实现实时通信功能的完整指南》,作者:柠檬味拥抱。在现代Web应用程序中,实时通信已经成为了必不可少的功能之一。无论是在线聊天、实时数据更新还是实时通知,都需要通过实时通信技术来实现。Django作为一个强大的Web框架,提......
  • FFT转载
    快速傅里叶变换(FFT)详解原文链接:快速傅里叶变换(FFT)详解-自为风月马前卒-博客园(cnblogs.com)目录前言多项式系数表示法点值表示法复数向量圆的弧度制平行四边形定则复数运算法则单位根单位根的性质快速傅里叶变换快速傅里叶逆变换理论总结......
  • ASP.NET MVC4.0+EF+LINQ+bui+bootstrap+网站+角色权限管理系统(1)
    ASP.NETMVC4.0+EF+LINQ+bui+bootstrap+网站+角色权限管理系统(1) 本系列的的角色权限管理主要采用Dotnet MVC4工程内置的权限管理模块Simplemembership实现,主要有关文件是InitializeSimpleMembershipAttribute.cs和AccountModels.cs下面是对这两个文件的了解和改造 WebSe......
  • java 异步任务,定时任务,邮件发送
    java异步任务,定时任务,邮件异步任务异步方法注解:@Async主程序开启异步注解功能:@EnableAsync定时任务开启定时功能的注解在main:@EnableSchedulingTaskScheduler:任务调用者TaskExecutor:任务执行者@EnableScheduling开启定时功能的注解@Scheduled固定......
  • ASP.NET MVC4.0+EF+LINQ+bui+网站+角色权限管理系统(6)
    ASP.NETMVC4.0+EF+LINQ+bui+网站+角色权限管理系统(6) 快过年了,公司事情忙,好几天没有继续写博客,今天开始写账户模块系统登录,账户管理以及登录日志,首先新建登录日志数据表: ViewCode然后更改模型:AccountModels.cs ViewCode创建登录日志模型:M_UsersLoginLogs.cs View......
  • C# 异步编程Task(三) async、await
    一、async和await两个修饰符C#5.0的时候引入了async和await两个修饰符,成为异步编程的核心关键字。async是修饰符,表明方法含有异步操作,但并不是说整个方法是异步的。async修饰的方法会先同步执行到第一处await的地方而后开始异步。await可以理解为一异步特有的“return”。即返回......
  • 转载Using Domain-Driven Design(DDD)in Golang
    转载自:https://dev.to/stevensunflash/using-domain-driven-design-ddd-in-golang-3ee5UsingDomain-DrivenDesign(DDD)inGolang#go#ddd#redis#postgresDomain-DrivenDesignpatternisthetalkofthetowntoday.Domain-DrivenDesign(DDD)isanapproachtosoft......
  • 【转载】Java函数式编程——为什么new Thread()中要用函数式编程
    面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,......
  • 【转载】WPF中Binding使用StringFormat格式化字符串方法
    原文链接:https://www.cnblogs.com/xuliming/articles/StringFormat.htmlWPF中Binding使用StringFormat格式化字符串方法 货币格式<TextBlockText="{BindingPrice,StringFormat={}{0:C}}"/>//$123.46货币格式,一位小数<TextBoxText="{BindingPrice,Stri......