C# 异步编程基础(七)异步原理

此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019

参考资料:https://www.bilibili.com/video/BV1Zf4y117fs

目录

C# 异步编程基础(一)线程和阻塞

C# 异步编程基础(二)线程安全、向线程传递数据和异常处理

C# 异步编程基础(三)线程优先级、信号和线程池

C# 异步编程基础(四) 富客户端应用程序的线程 和 同步上下文 Synchronization Contexts

C# 异步编程基础(五)Task

C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay

C# 异步编程基础(七)异步原理

C# 异步编程基础(八) 异步函数

C# 异步编程基础(九) 异步中的同步上下文、ValueTask

C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器

同步和异步

  1. 同步操作会在返回调用者之前完成它的工作
  2. 异步操作会在返回调用者之后去做它的(大部分)工作
    异步发方法更为少见,会启用并发,因为它的工作会与调用者并行执行
    异步的方法通常很快(立即)就返回到调用者,所以叫非阻塞方法
  3. 目前见到的大部分的异步方法都是通用目的的:
    Thread.Start
    Task.Run
    可以将Continuation附加到Task的方法
    ...

什么是异步编程

  1. 异步编程的原则是将长时间运行的函数写成异步的
  2. 传统的做法是将长时间运行的函数写出同步的,然后从新的线程或Task进行调用,从而按需引入并发
  3. 上述异步方式的不同之处在于,它是从长时间运行函数的内部启动并发。这有两点好处:
    IO-Bound并发可不使用线程来实现(用回调)。可提高可扩展和执行效率
    富客户端在worker线程会使用更少的代码,简化了线程安全性

异步编程的两种用途

  1. 编写高效处理大量并发IO的应用程序(典型的:服务端应用)
    挑战的并不是线程安全(因为共享状态通常是最小化的),而是执行效率
       特别的,每个网络请求并不会消耗一个线程
  2. 在富客户端应用里简化线程安全
    如果call graph(调用图)中任何一个操作是长时间运行的,那么整个call graph必须运行在worker线程上,以保证UI响应
       得到一个横跨多个方法的单一并发操作(粗粒度)
       需要为call graph中的每个方法考虑线程安全
    异步的call graph,直到需要的时候才开启一个线程,通常较浅(IO-Bound操作完全不需要)
       其它的方法可以在UI线程执行,线程安全简化
       并发的粒度适中
        一连串小的并发操作,操作之间会弹回到UI线程

经验之谈

  1. 为了获得上述好处,下列操作建议异步编写:
    IO-Bound和Complute-Bound操作
    执行超过50毫秒的操作
  2. 另一方面过细的粒度会损害性能,因为异步操作也有开销

异步编程和Continuation 以及语言的支持

  1. Task非常适合异步编程,因为它们支持Continuation(它对异步非常重要)
    第十六讲里面TaskCompletionSource的例子
    TaskCompletionSource是实现底层IO-Bound异步方法的一种标准方式
  2. 对于Compute-Bound方法,Task.Run会初始化绑定线程的并发
    把task返回调用者,创建异步方法
    异步编程的区别:目标是在调用图的较低位置来这样做
       富客户端应用中,高级方法可以保留在UI线程和访问控制以及共享状态上,不会出现线程安全问题

同步版

static void Main(string[] args)
{
    DisplayPrimeCounts();

    //粗粒度调用
    //Task.Run(() => DisplayPrimeCounts());
}
//相当于UI线程
static void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
        " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    }
    Console.WriteLine("Done!");
}

static int GetPrimesCount(int start, int count)
{
    return ParallelEnumerable.Range(start, count).Count(n =>
         Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
}

结果

异步版,但是输出不是我们想要的结果

static void Main(string[] args)
{
    DisplayPrimeCounts();
    //需要阻塞线程
    Console.ReadKey();
}
//相当于UI线程
static void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult() + " primes between ... "));
    }

    Console.WriteLine("Done!");
}

static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
           Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

结果

语言对异步的支持非常重要

  1. 上述例子
  2. 需要对task的执行序列化
    例如Task B依赖于Task A的执行结果
    上述例子为此,必须在continuation内部触发下一次循环

同步版

static void Main(string[] args)
{
    DisplayPrimeCounts();
    //需要阻塞线程
    Console.ReadKey();
}

static void DisplayPrimeCounts()
{
    DisplayPrimeCountsFrom(0);
}

static void DisplayPrimeCountsFrom(int i)
{
    var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
    //内部异步调用
    awaiter.OnCompleted(() =>
    {
        Console.WriteLine(awaiter.GetResult() + " primes between ... ");
        if (++i < 10)
        {
            DisplayPrimeCountsFrom(i);
        }
        else
        {
            Console.WriteLine("Done");
        }
    });
}

static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

结果

异步版,利用状态机,太麻烦

class Program
{
    static void Main(string[] args)
    {
        DisplayPrimeCountsAsync();
        //需要阻塞线程
        Console.ReadKey();
    }

    static Task DisplayPrimeCountsAsync()
    {
        var machine = new PrimesStateMachine();
        machine.DisplayPrimeCountsFrom(0);
        return machine.Task;
    }

    public static Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
               Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
}

class PrimesStateMachine
{
    TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
    public Task Task
    {
        get { return _tcs.Task; }
    }

    public void DisplayPrimeCountsFrom(int i)
    {
        var awaiter = Program.GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            Console.WriteLine(awaiter.GetResult());
            if (++i < 10)
            {
                DisplayPrimeCountsFrom(i);
            }
            else
            {
                Console.WriteLine("Done");
                _tcs.SetResult(null);
            }
        });
    }
}

结果

3、async和await
对于不想复杂的实现异步非常重要
原理:状态机
   异步编程时,异步函数被编译为包含状态机的形式,使得代码在某处停留,等待某个任务完成

修改后的例子

static async Task Main(string[] args)
{
    await DisplayPrimeCountsAsync();
}
async static Task DisplayPrimeCountsAsync()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    }
    Console.WriteLine("Done");
}
//凡是返回Task的方法都可以用await进行调用
static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

结果

异步原理 结束

posted @ 2021-02-09 16:42  .NET好耶  阅读(1866)  评论(1编辑  收藏  举报