C#异步编程学习笔记4 之 异步函数

异步函数

使用 async 和 awit 关键字可以写出和同步代码一样简介且结构相同的异步代码。

awaiting

await 关键字简化了附加 continuation 的过程。其结构如下:

var result = await expression;
statement(s);

它的作用相当于:

var awaiter = expression.GetAwaiter();
awaiter.OnCompleted(() => {
    var result = awaiter.GerResult();
    statement(s);
});

以下面代码为例,简要说明 async 和 awit 关键字的用法:

class Program
{
    static async Task Main(string[] args)
    {
        await DisplayPrimeCountsAsync();
    }

    async static Task DisplayPrimeCountsAsync()
    {
        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)));
    }
}
  • GetPrimesCountAsync 为异步方法,返回类型为 Task<int>。对于异步的方法,可以通过在方法名前面加上 await 关键字来调用。
    • 如果方法的返回值是 void,则无法使用 await 表达式来调用。
  • 而在包含 await 表达式的方法中,方法前必须使用 async 关键字来修饰。

async 修饰符

  • async 修饰符会让编译器把 await 当作关键字而不是标识符(因为 C# 5 以前,await 不是关键字,代码中可能会使用 await 作为标识符。
  • async 修饰符只能应用于方法(包括 lambda 表达式)。
    • 使用 async 修饰的方法可以返回 void、Task、Task<TResult>
  • asyne 修饰符对方法的签名或 public 源数据没有影响(和 unsafe 一样),它只会影响方法内部
    • 在接口内使用 async 是没有意义的
    • 使用 async 来重载非 async 的方法却是合法的(只要方法签名一致)
  • 使用了 async 修饰符的方法就是“异步函数”。

异步方法如何执行

遇到 await 表达式,执行(正常情况下)会返回调用者(与 Iterator 里面的 yield return 类似)

  • 在返回前,运行时会附加一个 continuation 到 await 的 task
    • 为保证 task 结束时,执行会跳回原方法,从停止的地方继续执行
  • 如果发生故障,那么异常会被重新抛出
  • 如果一切正常,那么它的返回值就会赋给 await 表达式

以下面代码为例,代码中 DisplayPrimeCount 方法与 DisplayPrimeCountsAsync 方法完全一致。

class Program
{
    static async Task Main(string[] args)
    {
        await DisplayPrimeCountsAsync();
    }
    
    void DisplayPrimeCount()
    {
        var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            int result = awaiter.GetResult();
            Console.WriteLine(result);
        });
    }

    async static Task DisplayPrimeCountsAsync()
    {
        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 什么?

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

捕获本地状态

await 表达式的最牛之处就是它几乎可以出现在任何地方。

特别的,在异步方法内,await 表达式可以替换任何表达式,除了 lock 表达式和 unsafe 上下文

class Program
{
    static async Task Main(string[] args)
    {
    }
    
    async void DisplayPrimeCounts()
    {
        //在下面的循环中,每次遇到 await 表达式,程序运行异步方法 GerPrimeCountAsync,
        //每次执行完之后,程序会回到 await 表达式,同时捕获方法本地变量 i
        for(int i = 0; i < 10; i++)
            Console.WriteLine(await GerPrimeCountAsync(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 之后在哪个线程上执行

在 await 表达式之后,编译器依赖于 continuation(通过 awaiter 模式)来继续执行

  • 如果在富客户端应用的 UI 线程上,同步上下文会保证后续是在原线程上执行;
  • 否则,就会在 task 结束的线程上继续执行。
UI 上的 await

下面代码是一个简单的 WPF 代码,运行操作之后,UI 线程会处于一个假死的状态,等待后台完成计算之后,页面会直接显示最终的计算结果。

//WPF
public partial class MainWindow : Window
{
	Button _button = new Button { Content = "Go" }; 

	TextBlock _results = new TextBlock();

    public MainWindowO
    {
        InitializeComponent();
        var panel = new StackPanel(); 
        panel.Children.Add(_button); 
        panel.Children.Add(_results);
        Content = panel;
        _button.Click += (sender, args) => Go();
    }
    
    void Go()
    {
        for(int i = 1; i < 5; i++)
            _results.Test += GetPrimesCount(i * 1000000, 1000000) +
            " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 -1) + Environment.NewLine;
    }
    
    int GetPrimesCount(int start, int count)
    {
        return ParallelEnumerable.Range(start, count).Count(n =>
        	Enumerable.Range(2, (int)Matb.Sqrt(n) - 1).All(i => n % i > 0));
    }
}

通过更改代码为异步形式,可以避免 UI 线程出现假死,页面可以一行一行输出结果,且不影响页面的其它操作。代码如下:

//WPF
public partial class MainWindow : Window
{
	Button _button = new Button { Content = "Go" }; 

	TextBlock _results = new TextBlock();

    public MainWindowO
    {
        InitializeComponent();
        var panel = new StackPanel(); 
        panel.Children.Add(_button); 
        panel.Children.Add(_results);
        Content = panel;
        _button.Click += (sender, args) => Go();
    }
    
    async Go()
    {
        _button.IsEnable = false;
        for(int i = 1; i < 5; i++)
            _results.Test += await GetPrimesCountAsync(i * 1000000, 1000000) +
            " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 -1) + Environment.NewLine;        
        _button.IsEnable = true;
    }
    
    Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
        	Enumerable.Range(2, (int)Matb.Sqrt(n) - 1).All(i => n % i > 0)));
    }
}

在本例中,只有 GetPrimesCountAsync 中的代码在 worker 线程上运行。Go 中的代码会“租用” UI 线程上的时间。可以说:Go 方法是在消息循环中“伪并发” 的执行。

  • 也就是说:Go 方法和 UI 线程处理的其它时间是穿插执行的
  • 因为这种伪并发,唯一能发生“抢占”的时刻就是在 await 期间。
    • 这其实简化了线程安全,防止重新进入即可(本例中即防止按钮被重复点击即可)

这种并发发生在调用栈较浅的地方(Task.Run 调用的代码里)。

为了从该模型中获益,真正的并发代码要避免访问共享状态或(共享的)UI 控件。

代码运行原理

上述并发过程可以用以下伪代码来表示原理:

为本线程设置同步上下文(WPF)
while(!程序结束)
{
	等着消息队列中发生一些事情
	发生了事情,是哪种消息?
	键盘/鼠标消息 -> 触发 event handler
	用户 BeginInvoke/Invoke 消息 -> 执行委托
}
  • 附加到 UI 元素的 event handler 通过消息循环执行
    • 上述代码中,当 Go 函数运行时,会先一直运行到 await 语句
    • await 语句会将 UI 释放出来,让 UI 可以响应其它事件。但是编译器会保证在 await 返回之前,设置好一个 continuation,以便在任务完成的时候,能够接着刚刚停止的地方继续往下执行
  • 因为在 UI 线程上 await,continuation 将发送到同步上下文中,该同步上下文通过消息循环执行,来保证整个 Go 方法伪并发的在 UI 线程上执行。
与粗粒度的并发相比

使用 BackgroundWorker 是粗粒度的并发。(Task.Run 与 BackgroundWorker 效果基本一致,但Task.Run 使用更简便,下面以 Task.Run 为例。)

//WPF
public partial class MainWindow : Window
{
	Button _button = new Button { Content = "Go" }; 

	TextBlock _results = new TextBlock();

    public MainWindowO
    {
        InitializeComponent();
        var panel = new StackPanel(); 
        panel.Children.Add(_button); 
        panel.Children.Add(_results);
        Content = panel;
        _button.Click += (sender, args) => 
        {
            _button.IsEnabled = false;
            Task.Run(() => Go());
        };        
    }
    
    void Go()
    {
        for(int i = 1; i < 5; i++)
        {
            int result = GetPrimesCount(i * 1000000, 1000000);
            Dispatcher.BeginInvoke(new Action(() =>
            	 _results.Test += result + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 -1) + Environment.NewLine));
        }
        Dispatcher.BeginInvoke(new Action(() => _button.IsEnabled = true));
    }
    
    int GetPrimesCount(int start, int count)
    {
        return ParallelEnumerable.Range(start, count).Count(n =>
        	Enumerable.Range(2, (int)Matb.Sqrt(n) - 1).All(i => n % i > 0));
    }
}

如上代码:

  • 整个同步调用图都在 worker 线程上
  • 必须在代码中到处使用 Dispatcher.BeginInvoke,否则 UI 线程无法响应更改
  • 循环本身在 worker 线程上
  • 引入了 race condition(静态条件)
  • 若实现取消和过程报告功能,会使得线程安全问题更容易发生,即使在方法中新添加任何的代码也是如此。(这就是在调用图中比较高的位置启用线程带来的危险)

编写异步函数

  • 对于任何异步函数,可以使用 Task 代替 void 作为返回类型,让该方法成为更有效的异步(可以进行 await)。

    static async Task Main(string[] args)
    {
        await PrintAnswerToLife();
        //此处不加 await 会报警告。
        //如果不加 await,PrintAnswerToLife 方法与 Main 就是并行执行的,而不是顺序执行。
    }
    
    static async Task PrintAnswerToLife()
    {
        await Task.Delay(5000);
        int answer = 21 + 2;
        Console.WriteLine(answer);
    }
    
  • 并不需要在方法体中显式的返回 Task。编译器会生成一个 Task(当方法完成或发生异常时),这使得创建异步的调用链非常方便。如下展示调用链的代码:

    static async Task Main(string[] args)
    {
        await Go();
    }
    
    async Task Go()
    {
        await PrintAnswerToLife();
        Console.WriteLine("Done");
    }
    
    async Task PrintAnswerToLife()
    {
        await Task.Delay(5000);
        int answer = 21 + 2;
        Console.WriteLine(answer);
    }
    
  • 编译器会对返回 Task 的异步函数进行扩展,使其成为当发送信号或发生故障时使用 TaskCompletionSource 来创建 Task 的代码。

    static async Task Main(string[] args)
    {
    }
    
    Task PrintAnswerToLife()
    {
        var tcs = new TaskCompletionSource<objcet>();
        var awaiter = Task.Delay(5000).GetAwaiter();
        awaiter.OnCompleted(() =>
                            {
                                try
                                {
                                    awaiter.GetResult();
                                    int answer = 21 * 2;
                                    Console.WriteLine(answer);
                                    tcs.SetResult(null)l
                                }
                                catch(Exception ex)
                                {
                                    tcs.SetException(ex);
                                }
                            });
        return tcs.Task;
    }
    
  • 因此,当返回 Task 的异步方法结束的时候,执行就会跳回到对它进行 await 的地方继续执行(通过 continuation)

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

富客户端场景下,执行在此刻会跳回到 UI 线程(如果目前不再 UI 线程的话)。

否则,就会在 continuation 返回的任意线程上继续执行。

这就意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是 UI 线程启动的第一次“反弹”。

返回 Task<TResult>

如果原函数方法体返回 TResult,那么异步方法就可以返回 Task<TResult>。

其原理就是给 TaskCompletionSource 发送信号带有值,而不是 null ;

static async Task Main(string[] args)
{
    int answer = await GetAnswerToLife();  //返回 int 类型
    //Task<int> answer = GetAnswerToLife();  //返回 Task<int> 类型
    Console.WriteLine(answer);
}

async Task<int> GetAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 + 2;
    return answer;
}

其实上述异步代码与同步编程相似,是 .Net 故意这样设计的。上述代码的同步版本如下:

static async Task Main(string[] args)
{
}

void Go()
{
    PrintAnswerToLife();
    Console.WriteLine("Done");
}

void PrintAnswerToLife()
{
    int answer = GetAnswerToLife();
    Console.WriteLine(answer);
}

int GetAnswerToLife()
{
    Thread.Sleep(5000);
    int answer = 21 + 2;
    return answer;
}
C# 中如何设计异步函数
  • 先以同步的方式编写方法
  • 使用异步调用来代替同步调用,并且进行 await
  • 除了顶层方法外(UI 控件的 event handler),把你方法的返回类型升级为 Task 或 Task<TResult>,这样它们就可以进行 await 了。
编译器能对异步函数生成 Task 意味着什么?
  • 大多数情况下,只需要在初始化 IO-bound 并发的底层方法里显式的初始化 TaskCompletionSource。这种情况很少见。
  • 针对初始化 compute-bound 的并发方法,可以使用 Task.Run 来创建 Task。
异步调用图执行

如下异步代码:

static async Task Main(string[] args)
{
    await Go();  // main thread
}

static async Task Go()
{
    var task = PrintAnswerToLife();
    await task;  //此处在 PrintAnswerToLife() 方法调用后才进行 await 的,说明方法 PrintAnswerToLife() 是在 main thread 上同步执行的
    Console.WriteLine("Done");
}

static async Task PrintAnswerToLife()
{
    var task = GetAnswerToLife();
    int answer = await task;  // 同 Go() 中的注释类似
    Console.WriteLine(answer);
}

static async Task<int> GetAnswerToLife()
{
    var task = Task.Delay(5000);  //在此处创建了一个 continuation
    await task;
    int answer = 21 + 2;
    return answer;
}
  • 整个代码执行与之前同步例子中调用图执行的顺序一样,因为我们对每个异步函数的调用都进行了 await。
  • 在调用图中创建了一个没有并行和重叠的连续流。
  • 每个 await 在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执行
并行(Parallelism)

不是用 await 来调用异步函数会导致并行执行的发生

  • 例如:_button.Click += (sender, args) => Go();
  • 这样确实也能满足保持 UI 响应的并发要求,UI 不会假死

同样的,可以并行两个操作,如下:

var task1 = PrintAnswerToLife();
var task2 = PrintAnswerToLife();
//await task1; await task2;  //如果此处不增加着两句 await,则前面的代码是并行执行的
异步 Lambda 表达式
  • 匿名方法(包括 Lambda 表达式),通过使用 async 也可以变成异步方法。调用方式也是一样的。

    static async Task Main(string[] args)
    {
        Func<Task> unnamed = async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine("Foo");
        };
        
        await NameMethod();
        await unnamed();
    }
    
    static async Task NameMethod()
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo");
    }
    
  • 附加 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";
    };
    
  • 也可以返回 Task<TResult>。

    static async Task Main(string[] args)
    {
        Func<Task<int>> unnamed = async () =>
        {
            await Task.Delay(1000);
            return 123;
        };
        int answer = await unnamed();
    }
    

异步中的同步上下文

发布异常
  • 富客户端应用通常依赖于集中的异常处理事件来处理 UI 线程上未捕获的异常。

    • 例如 WPF 中的 Application.DispatcherUnhandledException
    • ASP.NET Core 中定制 ExceptionFilterAttribute 也是差不多的效果

    其内部原理是:通过在它们自己的 try/catch 块来调用 UI 事件(在 ASP.NET Core 里就是页面处理方法的管道)

  • 顶层的异步方法会使事情更加复杂,如下代码:

    async void ButtonClick(object sender, RoutedEventArgs args)
    {
        await Task.Delay(1000);
        throw new Exception("Will this be ignored?");
    }
    
    • 当点击按钮,event handler 运行时,在 await 后,执行会正常地返回到消息循环;1 秒钟后抛出的异常无法被消息循环中的 catch 块捕获。

    • 为了缓解该问题,AsyncVoidMethodBuilder 会捕获未处理的异常(在返回 void 的异步方法里),并把它们发布到同步上下文(如果出现的话),以确保全局异常处理事件能够触发。

      【注意】

    • 编译器只会把上述逻辑应用于返回类型为 void 的异步方法

    • 如果 ButtonClick 的返回类型是 Task,那么未处理的异常将导致结果 Task 出错,然后 Task 无处可去(导致未观察到的异常)

  • 一个有趣的细微差别:无论在 await 前面还是后边抛出异常,都没有区别。

    async void Foo(){ throw null; await Task.Delay(1000); }
    
    • 在上例中,异常会被发布到同步上下文(如果出现的话),而不会发布给调用者。
    • 如果同步上下文没有出现,异常将会在线程池上传播,从而终止应用程序。
  • 不直接将异常抛出回调用者的原因是为了确保可预测性和一致性。

    • 在下例中,不管 someCondition 是什么值,InvalidOperationException 将始终得到和导致 Task 出错同样的效果。

      async Task Foo()
      {
          if(someCondition) await Task.Delay(100);
          throw new InvalidOperationException();
      }
      
  • Iterator 也是一样的:

    IEnumerable<int> Foo() { throw null; yeild return 123; }
    
    • 在本例中,异常绝不会直接返回给调用者,直到序列被便利后,才会抛出异常。
OperationStarted 和 OperationCompleted

如果存在同步上下文,返回 void 的异步函数也会在进入函数是调用其 OperationStarted 方法,在函数完成时调用其 OperationCompleted 方法。

如果为了对返回 void 的异步方法进行单元测试而编写一个自定义的同步上下文,那么重写者两个方法确实有用。

优化——同步完成

异步函数可以在 await 之前就返回。

static async Task Main(string[] args)
{
    Console.WriteLine(await GetWebPageAsync("http://oreilly.com"));
}

static Dictionary<string, string> _cache = new Dictionary<string, string>();

static async Task<string> GetWebPageAsync(string uri)
{
    string html;
    if(_cache.TryGetValue(uri, out html))
        return html;
    return _cache[uri] = await new WebClient().DownloadStringTaskAsync(uri);
}
  • 如果 URI 在缓存中存在,那么不会有 await 发生,执行就会返回给调用者,方法会返回一个已经设置信号的 Task,这就是同步完成。
  • 当 await 同步完成的 Task 时,执行不会返回到调用者,也不会通过 continuation 跳回。它会立即执行到下个语句。

编译器是如何完成这个优化的?

  • 编译器是通过检测 awaiter 上的 IsCompleted 属性来实现这个优化的。也就是说。无论何时,当程序 await 的时候:

    Console.WriteLine(await GerWebPageAsync("http://oreilly.com"));
    
  • 如果是同步完成,编译器会释放可短路 continuation 的代码

    var awaiter = GetWebPageAsync().GetAwaiter();
    if(awaiter.IsCompleted)
        Console.WriteLine(awaiter.GetResult());
    else
        awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult()));
    

【注意】

  • 对一个同步返回的异步方法进行 await,仍然会引起一个小的开销(20纳秒左右,2019年的PC)
  • 反过来,跳回到线程池,会引入上下文切换开销,可能是 1-2 毫秒
  • 而跳回到 UI 的消息循环,至少是 10 倍开销(如果 UI 繁忙,那时间更长)
其它情况
  • 编写完全没有 await 的异步方法也是合法的,但是编译器会发出警告;

    async Task<string> Foo() { return "abc"; }
    

    这类方法可以用于重载 virtual/abstract 方法。

  • 另外一种可以达到相同结果的方式是:使用 Task.FromResult,它会返回一个已经设置好信号的 Task。

    Task<string> Foo() { return Task.FromResult("abc"); }
    
  • 如果从 UI 线程上调用,那么 GetWebPageAsync 方法是隐式线程安全的。您可以连续多次调用它(从而启动多个并发下载),并且不需要 lock 来保护缓存。

    • 但是,如果连续调用这个方法传入的是同一个网址的话,那么就相当于启动了多个冗余的下载,这样就比较低效了
  • 有一种简单的方法可以实现这一点,而不必求助于 lock 或信令结构。我们创建一个“futures” (Task<string>) 的缓存,而不是字符串的缓存。注意,并没有 async:

    static Dictionary<string, Task<string>> _cache = new Dictionary<string, Task<string>>();
    
    Task<string> GetWebPageAsync(string uri)
    {
        if(_cache.TryGetValue(uri, out var downloadTask))
            return downloadTask;
        return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri); //这里没有加 await,返回的也是一个 Task<string>。
    }
    
    • 这里没有加 await,返回的也是一个 Task<string>。
    • 这个时候如果重复调用 GetWebPageAsync,代码会进入 if 分支,返回的是同一个 Task<string>。
    • 而且如果这个 Task 已经 Completed 了,那么它的 await 的开销也会非常小。
不使用同步上下文,使用 lock 也可以

lock 的不是下载的过程,lock 的是检测缓存的过程(很短暂)

lock (_cache)
{
    if(_cache.TryGetValue (uri, out var downloadTask))
        return downloadTask;
    else
        return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri);
}

ValueTask<T>

ValueTask<T> 用于微优化场景,使用场景非常少,可能永远不需要编写返回此类型的方法。

Task<T> 和 Task 是引用类型,实例化它们需要基于堆的内存分配和后续的(垃圾)收集。

优化此问题的一种极端形式是编写无需分配此类内存的代码;换句话说,这不会实例化任何引用类型,不会给垃圾收集增加负担。

为了支持这种模式,C# 引入了 ValueTask 和 ValueTask<T> 这两个 struct,编译器允许使用它们代替 Task 和 Task<T>

如下例子:

async ValueTask<int> Foo(){...}
  • 如果上例操作是同步完成,则 await ValueTask<T> 是无分配的(无需分配堆内存)。如下:

    int answer = await Foo();  //(可能是)无分配的
    
  • 如果操作不是同步完成的,ValueTask<T> 实际使就会创建一个普通的 Task<T>(并将 await 转发给它)

  • 使用 AsTask 方法,可以把 ValueTask<T> 转化为 Task<T>(也包括非泛型版本)

使用 ValueTask<T> 时的注意事项
  • ValueTask<T> 并不常见,它的出现纯粹时为了性能。

  • 这意味着它被不恰当的值类型语义所困扰,这可能会导致意外。为避免错误行为,必须避免以下情况:

    • 避免多次 await 同一个 ValueTask<T>
    • 避免操作没结束的时候就调用 .GetAwaiter() 和 .GetResult() 方法
  • 如果需要进行以上两种操作,那么先调用 AsTask 方法,操作它返回的 Task。

  • 避免上述陷阱最简单的办法就是直接 await 方法调用:

    • await Foo();
  • 将 ValueTask 赋给变量时,就可能引发错误了:

    • ValueTask<int> valueTask = Foo();
  • 将其立即转化为普通的 Task,就可以避免此类错误的发生:

    • Task<int> task = Foo().AsTask();
避免过度的弹回

对于在循环中多次调用的方法,通过调用 ConfigureAwait 方法,就可以避免重复的探花到 UI 消息队列所带来的开销。

这强迫 Task 不把 continuation 弹回给同步上下文。从而将开销削减到接近上下文切换的成本(如果 await 的方法同步完成,则开销会小得多)

async void A() { ... await B(); ...}

async Task B()
{
    for(int i = 0; i < 1000: i++)
        await C().ConfigureAwait(false);
}

async Task C() { ... }
  • 这意味着对于方法 B 和 C,我们取消了 UI 线程中的简单线程安全模型,即代码在 UI 线程上运行,并且只能在 await 语句期间被抢占。但是,方法 A 不受影响,如果在一个 UI 线程上启动,它将保留在 UI 线程上。
  • 这种优化在编写库时特别重要:您不需要简化线程安全性带来的好处,因为您的代码通常不与调用方共享状态,也不访问 UI 控件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值