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组合器

异步函数

async和await关键字可以让你写出和同步代码一样简洁且结构相同的异步代码

await

  1. await关键字简化了附加continuation的过程
  2. 其结构如下:
    var result=await expression;
    statement(s);
  1. 它的作用相当于:
var awaiter=expression.GetAwaiter();
awaiter.OnCompleted(()=>
{
    var result=awaiter.GetResult();
    statement(s);
});

例子

static async Task Main(string[] args)
{

}
//使用await的函数一定要async修饰
//await不能调用无返回值的函数
static async Task DisplayPrimesCountAsync()
{
    int result = await GetPrimesCountAsync(2, 1000000);
    Console.WriteLine(result);
}

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)));
}

async修饰符

  1. async修饰符会让编译器把await当作关键字而不是标识符(C# 5 以前可能会使用await作为标识符)
  2. async修饰符只能应用于方法(包括lambda表达式)
    该方法可返回void、Task、Task
  3. async修饰符对方法的签名或public元数据没有影响(和unsafe一样),它只会影响方法内部
    在接口内使用async是没有意义的
    使用async来重载非async的方法却是合法的(只要方法签名一致)
  4. 使用了async修饰符的方法就是“异步函数”
  5. 假设有一个async函数Foo,如果不使用await调用Foo,在没有Task.Run的情况下是顺序执行,但是如果Foo中有await调用其它async函数,那么就会先继续执行Foo之后的代码,这就是非阻塞

异步方法如何执行

  1. 遇到await表达式,执行(正常情况下)会返回调用者
    就像iterator里面的yield return
    在返回前,运行时会附加一个continuation到await的task
       为了保证task结束时,执行会跳回原方法,从停止的地方继续执行
    如果发生故障,那么异常就会被重新抛出
    如果一切正常,那么它的返回值就会赋值给await表达式

例子

static async Task Main(string[] args)
{

}
//两种方法作用相同
static void DisplayPrimesCount()
{
    var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        int result = awaiter.GetResult();
        Console.WriteLine(result);
    });
}

static async Task DisplayPrimesCountAsync()
{
    int result = await GetPrimesCountAsync(2, 1000000);
    Console.WriteLine(result);
}

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)));
}

可以await什么?

  1. 你await的表达式通常是一个task
  2. 也可以满足下列条件的任意对象:
    有GetAwaiter方法,它返回一个awaiter(实现了INotifyCompletion.OnCompleted接口)
    返回适当类型的GetResult方法
    一个bool类型的IsCompleted属性

捕获本地状态

  1. await表达式最牛之处就是它几乎可以出现在任何地方
  2. 特别的,在异步方法内,await表达式可以替换任何表达式,除了lock表达式和unsafe上下文

例子

static async Task Main(string[] args)
{

}
static async void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000));
    }
}
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)));
}

await之后在哪个线程上执行

  1. 在await表达式之后,编译器依赖于continuation(通过awaiter模式)来继续执行
  2. 如果在富客户端的UI线程上,同步上下文会保证后续是在原线程上执行
  3. 否则,就会在task结束的线程上继续执行

UI上的await

  1. 例子,建议这样写异步函数
public MainWindow()
{
    InitializeComponent();

}

async void Go()
{
    this.Button1.IsEnabled = false;

    for (int i = 1; i < 5; i++)
    {
        this.TextMessage.Text += await this.GetPrimesCountAsync(i * 1000000, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine;
    }

    this.Button1.IsEnabled = true;
}

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)));
}

private void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Go();
}
  1. 本例中,只有GetPeimesCountAsync中的代码在worker线程上运行
  2. Go中的代码会“租用”UI线程上的时间
  3. 可以说,Go是在消息循环中“伪并发”的执行
    也就是说:它和UI线程处理的其它时间是穿插执行的
    因为这种伪并发,唯一能发生“抢占”的时刻就是在await期间,这其实简化了线程安全,防止重新进入即可
  4. 这种并发发生在调用栈较浅的地方(Task.Run调用的代码里)
  5. 为了从该模型获益,真正的并发代码要避免访问共享状态或UI控件

例子

async void Go()
{
    this.Button1.IsEnabled = false;

    string[] urls = "www.bing.com www.baidu.com www.cnblogs.com".Split();
    int totalLength = 0;
    try
    {
        foreach (string url in urls)
        {
            var uri = new Uri("http://" + url);
            byte[] data = await new WebClient().DownloadDataTaskAsync(uri);
            this.TextMessage.Text += "Length of " + url + " is " + data.Length + Environment.NewLine;
            totalLength += data.Length;
        }
        this.TextMessage.Text += "Total length " + totalLength;
    }
    catch (WebException e)
    {
        this.TextMessage.Text += "Error:" + e.Message;
    }
    finally
    {
        this.Button1.IsEnabled = true;
    }

}

private void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Go();
}

伪代码:

为本线程设置同步上下文(WPF)
while(!程序结束)
{
    等着消息队列中发生一些事情
    发生了事情,是哪种消息?
    键盘/鼠标消息->触发event handler
    用户BeginInvoke/Invoke 消息->执行委托
}

   附加到UI元素的event handler通过消息循环执行
   因为在UI线程上await,continuation将消息发送到同步上下文上,该同步上下文通过消息循环执行,来保证整个Go方法伪并发的在UI线程上执行

与粗粒度的并发相比

1、例如使用BackgroundWorker,不推荐这样写异步函数

void Go()
{
    for (int i = 1; i < 5; i++)
    {
        int result = this.GetPrimesCount(i * 1000000, 1000000);
        this.Dispatcher.BeginInvoke(new Action(() =>
        this.TextMessage.Text += result + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine));
    }
    this.Dispatcher.BeginInvoke(new Action(() => this.Button1.IsEnabled = true));
}

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));
}

private async void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Button1.IsEnabled = false;

    Task.Run(() => this.Go());
}
  1. 整个同步调用图都在worker线程上
  2. 必须在代码中到处使用Dispatcher.BeginInvoke
  3. 循环本身在worker线程上
  4. 引入了race condition
  5. 若实现取消和过程报告,会使得线程安全问题更任意发生,在方法中新添加任何的代码也是同样的效果

编写异步函数

  1. 对于任何异步函数,你可以使用Task替代void作为返回类型,让该方法成为更有效的异步(可以进行await)
    例子
static async Task Main(string[] args)
{
    //不加await关键字就是并行,不会等待
    await PrintAnswerToLife();
}
static async Task PrintAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    Console.WriteLine(answer);
}
  1. 并不需要在方法体中显式的返回Task。编译器会生成一个Task(当方法完成或发生异常时),这使得创建异步的调用链非常方便
    例子
static async Task Main(string[] args)
{
}
static async Task Go()
{
    await PrintAnswerToLife();
    Console.WriteLine("Done");
}
static async Task PrintAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    Console.WriteLine(answer);
}
  1. 编译器会对返回Task的异步函数进行扩展,使其成为当发送信号或发生故障时使用TaskCompletionSource来创建Task的代码
    大致代码
static Task PrintAnswerToLide()
{
    var tcs = new TaskCompletionSource<object>();
    var awaiter = Task.Delay(5000).GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        try
        {
            awaiter.GetResult();
            int answer = 21 * 2;
            Console.WriteLine(answer);
            tcs.SetResult(null);
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    return tcs.Task;
}
  1. 因此,当返回Task的异步方法结束的时候,执行就会跳回到对它进行await的地方(通过continuation)

编写异步函数,富客户端场景下

  1. 富客户端场景下,执行在此刻会跳回到UI线程(如果目前不在UI线程的话)
  2. 否则,就在continuation返回的任意线程上继续执行
  3. 这意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是UI线程启动的第一次“反弹”

返回Task

  1. 如果方法体返回TResult,那么异步方法就可以返回Task
    例子
static async Task Main(string[] args)
{

}
static async Task<int> GetAnswerToLiife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    return answer;
}
  1. 其原理就是给TaskCompletion发送的信号带有值,而不是null
    例子
static async Task PrintAnswerToLife()
{
    int answer = await GetAnswerToLife();
    Console.WriteLine(answer);
}
static async Task<int> GetAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    return answer;
}
  1. 与同步编程很相似,是故意这样设计的
    同步版本
static void Main(string[] args)
{

}
static void Go()
{
    PrintAnswerToLife();
    Console.WriteLine("Done");
}
static void PrintAnswerToLife()
{
    int answer = GetAnswerToLife();
    Console.WriteLine(answer);
}
static int GetAnswerToLife()
{
    Thread.Sleep(5000);
    int answer = 21 * 2;
    return answer;
}

C#中如何设计异步函数

  1. 以同步的方式编写方法
  2. 使用异步调用来替代同步调用,并且进行await
  3. 除了顶层方法外(UI控件的event handler,因为没有await调用),把你方法的返回类型升级为Task或Task,这样它们就可以进行await了

编译器能对异步函数生成Task意味着什么?

  1. 大多数情况下,你只需要在初始化IO-Bound并发的底层方法里显式的初始化TaskCompletionSource,这种情况很少见
  2. 针对初始化Compute-Bound的并发方法,你可以使用Task.Run来创建Task

异步调用图执行

例子

static async Task Main(string[] args)
{
    //Main Thread
    await Go();
}
static async Task Go()
{
    var task = PrintAnswerToLife();
    await task;
    Console.WriteLine("Done");
}
static async Task PrintAnswerToLife()
{
    var task = GetAnswerToLife();
    int answer = await task;
    Console.WriteLine(answer);
}
static async Task<int> GetAnswerToLife()
{
    var task = Task.Delay(5000);
    await task;
    int answer = 21 * 2;
    return answer;
}
  1. 整个执行与之前同步例子中调用图的执行顺序是一样的,因为我们对每个异步函数的调用都进行了await
  2. 在调用图中创建了一个没有并行和重叠的连续流
  3. 每个await在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执

并行(Parallelism)

  1. 不使用await来调用异步函数会导致并行执行的发生
  2. 例如:_button.Click+=(sender,args)=>Go();
    主线程仍然在执行,GO()也在执行
    确实也能满足保持UI响应的并发要求
  3. 同样,可以并行跑两个操作:
    var task1=PrintAnswerToLife();
    var task2=PrintAnswerToLife();
    await task1;
    await task2;

异步Lambda表达式

  1. 匿名方法(包括Lambda表达式),通过使用async也可以变成异步方法
  2. 调用方式也一样
static async Task Main(string[] args)
{
    Func<Task> unnamed = async () =>
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo");
    };

    await NamedMethod();
    await unnamed();
}
static async Task NamedMethod()
{
    await Task.Delay(1000);
    Console.WriteLine("Foo");
}
  1. 附加event handler的时候也可以使用异步Lambda表达式

例子

myButton.Click+=async (sender,args)=>
{
    await Task.Delay(1000);
    myButton.Content="Done";
}

相当于

myButton.Click+=ButtonHandler;

async void ButtonHandler(object sender,EventArgs args)
{
    await Task.Delay(1000);
    myButton.Content="Done";
}
  1. 也可以返回Task
static async Task Main(string[] args)
{
    Func<Task<int>> unnamed = async () =>
    {
        await Task.Delay(1000);
        return 123;
    };
    int answer = await unnamed();
}

异步函数 结束

posted @ 2021-02-09 18:13  .NET好耶  阅读(3049)  评论(0编辑  收藏  举报