C# 8.0 新特性: Async Streams

参考link:

https://www.infoq.com/articles/Async-Streams/

中文博客中非常易懂的一篇:

https://www.cnblogs.com/CoderAyu/p/10680805.html

生产者和消费者的拉取模式以及推送模式

C# 8.0 中添加了一个新特性: Async Streams.

在了解这个新特性之前, 我们需要先了解什么是"推送和拉取机制".

在实际的生产环境或者商业逻辑中, 大多情况符合生产者和消费者的模式. 消费者消费生产者生产出来的产品. 但是两者之间始终会存在一些差异, 要么供不应求, 要么供大于求.

当生产者生产产品快, 而消费者消费产品慢时, 我们一般使用"拉取模式", 这种模式也是比较常见的模式, 只要消费者去要产品, 一般会有, 所以消费者一般不会有等待.

但是当消费者多消费快, 而生产者稀少生产慢时, 再继续使用"拉取模式", 消费者可能会排队等待, 这样的方式会造成消费者等待时间过长.

商业模式中这种现象会造成消费者不使用你的产品, 因为消费者往往是惰性的, 惰性往往不会去耐心等待产品, 所以基于这种情况, 我们一般使用"推送模式", 这种模式下, 一旦生产者生产出来产品, 就通过推送的方式推送给消费者, 消费者不需要去主动拉取产品.

下面这张图展示了消费者和生产者不均衡所采取的两种模式: 拉取和推送

基于惰性拉取机制的 Async Streams

Async 异步特性用于解决程序需要长时间运行去运算时防止主线程阻塞的解决方案

异步特性在 C# 5.0 就添加了. 使用 await 只能获取得到一个结果. 在 8.0 以后允许异步返回多个值. 从而提供了一种表示异步数据源的方式. (异步数据源可以理解为一个数据源通过异步不断地返回给主线程运算结果. 异步流是Java和JavaScript中使用的反应式编程模型的替代方案.)

C# 5 引入了Async/Await,用以提高用户界面响应能力和对Web资源的访问能力。换句话说,异步方法用于执行不阻塞线程并返回一个标量结果的异步操作。

现有异步方法的一个重要不足是它必须提供一个标量返回结果(一个值)。比如这个方法 async Task<int> DoAnythingAsync(),DoAnythingAsync的结果是一个整数(一个值)。

由于存在这个限制,你不能将这个功能与yield关键字一起使用,并且也不能将其与async IEnumerable<int>(返回异步枚举)一起使用。

C# 8 中新提出的 Async Streams 去掉了标量结果的限制,并允许异步方法返回多个结果。

为什么提供这种特性? 这要说道反应式编程.

Reactive Extensions(Rx)是解决异步编程问题的另一种方法。Rx 越来越受到开发人员的欢迎。很多其他编程语言(如 Java 和 JavaScript)已经实现了这种技术(RxJava、RxJS)。Rx 基于推送式编程模型(Push Programming Model),也称为反应式编程。反应式编程是事件驱动编程的一种类型,它处理的是数据而不是通知。

提供一些 cases 来引出主题

Case1

        /// <summary>
        /// simple foreach then return the result
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
       public  static int SumFromOneToCount(int count)
        {
            ConsoleExt.WriteLine("SumFromOneToCount called!");

            var sum = 0;
            for (var i = 0; i <= count; i++)
            {
                sum = sum + i;
            }
            return sum;
        }

        /// <summary>
        /// we use this method to get the data from producter, but if the producter's logic is complexity enough, 
        /// the main thread will be blocked for a long time, so that why we need the async to slove this problem.
        /// </summary>
        public void SumFromOneToCount() 
        {
            const int count = 5;
            ConsoleExt.WriteLine($"Starting the application with count: {count}!");
            ConsoleExt.WriteLine("Classic sum starting.");
            ConsoleExt.WriteLine($"Classic sum result: {Producter.SumFromOneToCount(count)}");
            ConsoleExt.WriteLine("Classic sum completed.");
            ConsoleExt.WriteLine("################################################");
            ConsoleExt.WriteLine(Environment.NewLine);
        }

Case2

        /// <summary>
        /// in this function, the result was splited by servral results and displayed in the consle.
        /// this is the benifit of yield feature. we can get some of the result before we get the whole result.
        /// but we can also see, the producter's logic still block the main thread.
        /// </summary>
        public void SumFromOneToCountYield() 
        {
            const int count = 5;
            ConsoleExt.WriteLine("Sum with yield starting.");
            foreach (var i in Producter.SumFromOneToCountYield(count))
            {
                ConsoleExt.WriteLine($"Yield sum: {i}");
            }
            ConsoleExt.WriteLine("Sum with yield completed.");

            ConsoleExt.WriteLine("################################################");
            ConsoleExt.WriteLine(Environment.NewLine);
        }

        /// <summary>
        /// simple foreach and yield return the result
        /// the caller can only get one result object is sum
        /// but will get the every iteration sum vaule because of yield statement
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
       public static IEnumerable<int> SumFromOneToCountYield(int count)
        {
            ConsoleExt.WriteLine("SumFromOneToCountYield called!");

            var sum = 0;
            for (var i = 0; i <= count; i++)
            {
                sum = sum + i;

                yield return sum;
            }
        }

Case3

        /// <summary>
        /// the async feature help us to slove the main thread block problem. 
        /// but we still got the whole result at one time.
        /// we need some solution to fix both main threads block and split the results.
        /// how to fix that requirement? 
        /// </summary>
        public async void SumFromOneToCountAsync() 
        {
            const int count = 5;
            ConsoleExt.WriteLine("async example starting.");
            var result = await Producter.SumFromOneToCountAsync(count);
            ConsoleExt.WriteLine("async Result: " + result);
            ConsoleExt.WriteLine("async completed.");

            ConsoleExt.WriteLine("################################################");
            ConsoleExt.WriteLine(Environment.NewLine);
        }

        /// <summary>
        /// async to get the sum value after a long foreach logic.
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public static async Task<int> SumFromOneToCountAsync(int count)
        {
            ConsoleExt.WriteLine("SumFromOneToCountAsync called!");

            var result = await Task.Run(() =>
            {
                var sum = 0;

                for (var i = 0; i <= count; i++)
                {
                    sum = sum + i;
                }
                return sum;
            });

            return result;
        }

Case4

        /// <summary>
        /// we used async method and will avoid to block the main thread, 
        /// also we can get the every iteration of the value, but it is come from one collection, and this collection will be get at one time.
        /// now we need a new mode: every time the producter create a product, this product should get back to the customer.
        /// </summary>
        public async void SumFromOneToCountTaskIEnumerable() 
        {
            const int count = 5;
            ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable started!");
            var scs = await Producter.SumFromOneToCountTaskIEnumerable(count);
            ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable done!");

            foreach (var sc in scs)
            {
                ConsoleExt.WriteLine($"AsyncIEnumerable Result: {sc}");
            }

            ConsoleExt.WriteLine("################################################");
            ConsoleExt.WriteLine(Environment.NewLine);
        }

         /// <summary>
        /// async method to get the every iteration of the sum value
        /// because all the iteration's sum value was added into a collection.
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<int>> SumFromOneToCountTaskIEnumerable(int count)
        {
            ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable called!");
            var collection = new Collection<int>();

            var result = await Task.Run(() =>
            {
                var sum = 0;

                for (var i = 0; i <= count; i++)
                {
                    sum = sum + i;
                    collection.Add(sum);
                }
                return collection;
            });

            return result;
        }

上面的四个 cases 请仔细阅读对应的 summary, 然后回过头来我们上述的 Cases, 能否对应上下列的几个商业需求:

  • 同步拉取

客户端向服务器端发送请求,客户端必须等待(客户端被阻塞),直到服务器端做出响应,如图所示。

  • 异步数据拉取

客户端发出数据请求然后继续执行其他操作。一旦有数据到达,客户端就继续处理达到的数据。

  • 异步序列数据拉取

客户端发出数据块请求,然后继续执行其他操作。一旦数据块到达,客户端就处理接收到的数据块并询问下一个数据块,依此类推,直到达到最后一个数据块为止。这正是 Async Streams 想法的来源。下图显示了客户端可以在收到任何数据时执行其他操作或处理数据块。

显然我们无法实现最后一个需求, 异步序列数据拉取. 这也是为什么我们需要 Async Streams, 其核心是:

        public interface IAsyncEnumerable<out T>
        {
            IAsyncEnumerator<T> GetAsyncEnumerator();
        }
        public interface IAsyncEnumerator<out T> : IAsyncDisposable
        {
            Task<bool> MoveNextAsync();
            T Current { get; }
        }
        // Async Streams Feature 可以被异步销毁 
        public interface IAsyncDisposable
        {
            Task DiskposeAsync();
        }

此时生产者的代码如下:

        /// <summary>
       /// 1. make this method to be an async method.
       /// 2. create task.delay to intent a long work for get the sum result.
       /// 3. use yield return to return the temp sum value to customer.
       /// </summary>
       /// <param name="count"></param>
       /// <returns></returns>
       public async static IAsyncEnumerable<int> SumFromOneToCountAsyncYield(int count)
       {
           ConsoleExt.WriteLine("SumFromOneToCountYield called!");
           var sum = 0;
           for (var i = 0; i <= count; i++)
           {
               ConsoleExt.WriteLine($"thread id: {System.Threading.Thread.GetCurrentProcessorId()},  current time: {DateTime.Now}");
               sum = sum + i;
               await Task.Delay(TimeSpan.FromSeconds(2));
               yield return sum;
           }
       }

消费者的代码如下:

        public static async Task SumFromOneToCountAsyncYield() 
        {
            const int count = 5;
            ConsoleExt.WriteLine("Sum with yield starting.");
            await foreach  (var i in Producter.SumFromOneToCountAsyncYield(count))
            {
                  ConsoleExt.WriteLine($"Yield sum: {i}");
            }
            ConsoleExt.WriteLine("Sum with yield completed.");

            ConsoleExt.WriteLine("################################################");
            ConsoleExt.WriteLine(Environment.NewLine);
        }

所以 Async Streams 为我们解决了什么问题?

这个还是比较关注的, 因为我们得知道在什么样的场景下, 去使用这个技术, 去解决一个什么样的实际问题.

根据 Async Streams 的技术解析来看, 由于这项技术本质上属于一种惰性拉取模式. 那么就是适用于解决异步延迟加载的问题,例如从网站下载数据或从文件或数据库中读取并记录.

行文最后附上我实践过的code link : https://github.com/itdennis/DennisDemos/blob/master/DennisCoreDemos/Csharp 8/Async streams/Producter.cs

posted @ 2019-09-25 17:06  YanyuWu  阅读(436)  评论(0编辑  收藏  举报