C#异步提示和技巧

编程

C#异步提示和技巧

原文地址:https://cpratt.co/async-tips-tricks/安全

将sync方法运行为“async”

Task.Run(() => DoSyncStuff());

从技术上讲,这是假的异步。它仍然阻塞,但它运行在后台线程上。这对于防止使用桌面/移动应用程序阻止UI线程很是有用。在Web应用程序上下文中,这几乎没有意义,由于每一个线程都来自同一个池,用于处理主(请求)线程来自的请求,而且在完成全部操做以前不会返回响应。app

将async方法做为同步运行

对于这个,咱们有一个从微软借来的助手类。它看起来是各类命名空间,但老是做为内部命名空间,所以您不能直接从框架中使用它。框架

public static class AsyncHelper  
{
    private static readonly TaskFactory _taskFactory = new
        TaskFactory(CancellationToken.None,
                    TaskCreationOptions.None,
                    TaskContinuationOptions.None,
                    TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); public static void RunSync(Func<Task> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); } 

而后异步

AsyncHelper.RunSync(() => DoAsyncStuff());

 

放弃上下文

当您await进行异步操做时,默认状况下会传递调用代码的上下文。这可能会对性能产生不小的影响。若是您之后不须要恢复该上下文,那么它只是浪费了资源。您能够经过附加ConfigureAwait(false)到您的通话来阻止此行为async

await DoSomethingAsync().ConfigureAwait(false);

你应该老是这样作,除非有特定的理由保持上下文。某些状况包括您须要访问特定GUI组件或须要从控制器操做返回响应时。异步编程

但重要的是,每一个操做都有本身的上下文,所以您能够安全地ConfigureAwait(false)在须要维护上下文的代码调用的异步方法中使用你只是没法使用ConfigureAwait(false)该方法自己。例如:post

public async Task<IActionResult> Foo() { // No `ConfigureAwait(false)` here await DoSomethingAsync(); return View(); } ... public async Task DoSomethingAsync() { // This is fine await DoSomethingElseAsync().ConfigureAwait(false); } 

所以,您能够而且应该将须要维护上下文的多个异步操做分解为单独的方法,所以您只须要保留上下文一次,而不是N次。例如:性能

public async Task<IActionResult> Foo() { await DoFirstThingAsync(); await DoSecondThingAsync(); await DoThirdThingAsync(); return View(); } 

在这里,每一个操做都得到调用代码的上下文的副本,而且因为咱们须要该上下文,所以使用ConfigureAwait(false)不是一个选项。可是,经过重构如下代码,咱们只须要调用代码的上下文的单个副本。google

public async Task DoThingsAsync()  
{
    await DoFirstThingAsync().ConfigureAwait(false);
    await DoSecondThingAsync().ConfigureAwait(false);
    await DoThirdThingAsync().ConfigureAwait(false);
}

public async Task<IActionResult> Foo() { await DoThingsAsync(); return View(); } 

 

异步和垃圾收集

在同步代码中,局部变量进入堆栈并在超出范围时被丢弃。可是,因为在等待异步操做时发生上下文切换,所以必须保留这些局部变量。框架经过将它们添加到堆上的结构来实现此目的。这样,当执行返回到调用代码时,能够恢复本地。可是,在代码中进行的操做越多,就必须将更多内容添加到堆中,从而致使更频繁的GC循环。其中一些多是不可避免的,可是当您要等待异步操做时,您应该注意无用的变量赋值。例如,代码如:

var today = DateTime.Today;
var todayString = today.ToString("MMMM d, yyyy");

这将致使两个不一样的值进入堆,而若是您只须要todayString,只需将代码重写为:

var todayString = DateTime.Today.ToString("MMMM d, yyyy");

除非有人告诉你,这是你没有想到的事情之一。

取消异步工做

C#中异步的一个好处是能够取消任务。若是用户在UI中取消任务,导航离开网页等,这容许您停止任务。要启用取消,您的异步方法应接受CancellationToken参数。

public async Task DoSomethingAsync(CancellationToken cancellationToken)  
{
    ...
}

而后,该取消令牌应该传递给该方法调用的任何其余异步操做。若是能够而且但愿取消,则该方法的责任是启用取消。并不是全部异步任务均可以取消。通常来讲,是否能够取消任务取决于该方法是否具备接受的重载CancellationToken

取消没法取消的任务

在某些状况下,若是方法未提供接受的重载,您仍能够取消任务CancellationToken你并无真正取消这项任务,可是根据实施状况,你可能会停止它,但仍然能够有效地得到相同的结果。例如,该ReadAsStringAsync方法HttpContent没有接受的重载CancellationToken可是,若是您丢弃了HttpResponseMessage,则会停止读取内容的尝试。

try  
{
    using (var response = await httpClient.GetAsync(new Uri("https://www.google.com")))
    using (cancellationToken.Register(response.Dispose))
    {
        return await response.Content.ReadAsStringAsync();
    }
}
catch (ObjectDisposedException)  
{
    if (cancellationToken.IsCancellationRequested)
        throw new OperationCanceledException();

    throw;
}

从本质上讲,咱们使用CancellationToken调用DisposeHttpResponseMessage状况下,若是它取消。这将致使ReadAsStringAsync抛出一个ObjectDisposedException咱们捕获了这个异常,若是CancellationToken已经取消,咱们会抛出异常OperationCanceledException

这种方法的关键在于可以处理某些父对象,这会致使没法取消的方法引起异常。它不适用于全部内容,但能够在某些状况下为您提供帮助。

等待asyncawait关键字

可使用如下任一方法编写异步方法:

public async Task FooAsync()  
{
    await DoSomethingAsync();
}

public Task BarAsync()  
{
    return DoSomethingAsync();
}

首先,在方法中等待异步操做,而后在返回到调用代码以前将结果包装在另外一个任务中。在第二步中,直接返回异步操做的任务。若是你有一个只调用另外一个异步方法的异步方法(一般是异步重载的状况),那么你应该忽略asyncawaitkeywords,就像上面的第二种方法同样。

处理异步方法中的异常

使用async关键字的方法能够安全地抛出异常。编译器将负责将异常包装在一个Task

public async Task FooAsync()  
{
    // This is fine
    throw new Exception("All your bases are belong to us.");
}

可是,Task没有async关键字的返回方法应该返回一个Task例外。

public Task FooAsync()  
{
    try
    {
        // Code that throws exception
    }
    catch (Exception e)
    {
        return Task.FromException(e);
    }
}

 

在实现方法的同步和异步版本时减小重复代码

一般在开发方法的同步和异步版本时,您会发现两个实现之间惟一真正的区别是,一个调用各类方法的异步版本,而另外一个调用同步版本。当实现几乎相同时,除了使用async / await以外,您能够利用各类“黑客”来分解重复的代码。我发现的最好和最少“hacky”方法被称为“Flag Argument Hack”。本质上,您引入了一个布尔值,指示该方法是应该使用同步仍是异步访问,而后相应地进行分支:

private async Task<string> GetStringCoreAsync(bool sync, CancellationToken cancellationToken) { return sync ? SomeLibrary.GetString() : await SomeLibrary.GetStringAsync(cancellationToken).ConfigureAwait(false); } public string GetString() => GetStringCoreAsync(true, CancellationToken.None) .ConfigureAwait(false) .GetAwaiter() .GetResult(); public Task<string> GetStringAsync() => GetStringAsync(CancellationToken.None); public Task<string> GetStringAsync(CancellationToken cancellationToken) => GetStringCoreAsync(false, cancellationToken); 

这彷佛是不少代码,因此让咱们解开它。首先,咱们有一个私人方法GetStringCoreAsync这是咱们分解公共代码的地方。在这里,咱们只是调用其余一些具备同步和异步方法的库来获取某种字符串。不能否认,对于这种简单化的东西,你真的不该该使用这个hack,而应该只是让每一个方法直接调用它的相应对应物。可是,我不想经过引入过于复杂的实现来阻碍理解。正如您所看到的,这里的要点是咱们正在分支sync使用库中的同步或异步方法的值。只要您等待异步方法,这将正常工做,这意味着此私有方法须要具备async关键字。咱们'CancellationToken 若是内部使用的异步方法是可取消的。

接下来,咱们只有调用私有方法的同步和异步实现。对于同步版本,咱们须要Task从私有方法中解包返回的内容。为此,咱们使用该GetAwaiter().GetResult()模式安全地阻止异步调用。这里没有死锁的危险,由于虽然私有方法是异步的,可是当咱们传递truesync,实际上并无使用异步方法。咱们还ConfigureAwait(false)用来防止附加同步上下文,由于它彻底没有必要膨胀:这里没有线程切换的可能性。

异步实现至关不起眼。CancellationToken.None若是没有传递取消令牌,则会有一个超时传递默认值,而后实际实现只是falsesync参数调用私有方法并包含取消令牌。

有一种思想流派认为方法不该该像这样的布尔分支。若是您有两组独立的逻辑,那么您应该有两个单独的方法。这有一些道理,但我认为必须权衡逻辑实际上有多么不一样。所以,若是您有大量重复的逻辑,这是分解公共代码的好方法。可是,它应该是这方面的最后手段。若是代码的某些部分是CPU绑定的或以其余方式同步运行,那么您应该首先尝试将这些代码部分分解出来。你的同步和异步方法之间可能仍然存在一些重复,可是若是你能够将大部份内容都放到可使用的方法而不诉诸黑客,那么这就是最佳路径。

还有一个论点要说,若是你有那么多的逻辑,你的方法可能首先作得太多了。你必须让本身的判断规则。有时候作这样的事情其实是最好的路径,可是在使用这种方法以前你应该仔细评估是不是这种状况。

控制台应用程序中的异步

class Program  
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }

    static async Task MainAsync(string[] args)
    {
        // await something
    }
}

对于它的价值,C#7.1承诺Async Main支持,因此你只需:

class Program  
{
    static async Task Main(string[] args)
    {
        // await something
    }
}

可是,在撰写本文时,这不起做用。不过,这真的只是语法糖。当编译器遇到异步Main时,它只是将它包装在常规同步Main中,就像在第一个代码示例中同样。

确保异步不会阻止

有不少术语与C#中的异步混淆。您据说同步代码会阻塞该线程,而异步代码则不会。这实际上不是真的。不管线程是否被阻止,实际上与同步仍是异步都没有任何关系。它进入讨论的惟一缘由是,若是你的目标是不阻塞线程,async至少比同步更好,由于有时候,在某些状况下,它可能只是在不一样的线程上运行。若是您的异步方法中有任何同步代码(任何不等待其余内容的代码),那么代码将始终运行同步。此外,若是等待的内容已经完成,则异步操做能够运行同步。最后,async不能确保工做不会在同一个线程上实际完成。它只是为线程切换开辟了可能性。

若是你须要确保异步操做不会阻塞线程,例如对于你想要保持GUI线程打开的桌面或移动应用程序,那么你应该使用:

Task.Run(() => DoStuffAsync());

等待。这与咱们上面用来运行同步“async”不同吗?是的。一样的原则适用:Task.Run将运行您在新线程上传递给它的委托。反过来,这意味着它不会在当前线程上运行。

使同步操做Task兼容

大多数异步方法返回Task,但并不是全部Task返回方法都必须是异步的。这可能有点使人费解。比方说,你须要实现一个返回的方法Task,但你实际上并不有什么异步作。Task<TResult>

public Task DoSomethingAsync(CancellationToken cancellationToken)  
{
    if (cancellationToken.IsCancellationRequested)
    {
        return Task.FromCanceled(cancellationToken);
    }

    try
    {
        DoSomething();
        return Task.FromResult(0);
    }
    catch (Exception e)
    {
        return Task.FromException(e);
    }
}

首先,这确保了操做没有被取消。若是有,则返回已取消的任务。而后,咱们须要作的同步工做包含在一个try..catch块中。若是抛出异常,咱们将须要返回一个包含该异常的错误任务。最后,若是它正确完成,咱们将返回一个已完成的任务。

重要的是要意识到这实际上并非异步。DoSomething仍然是同步并将阻止。可是,如今它能够像处理异步同样处理,由于它返回一个任务,就像它应该的那样。你为何要这样作?好吧,一个例子是在实现适配器模式时,您正在适应的其中一个源不提供异步API。您仍然必须知足接口,但您应该注释该方法以代表它实际上不是异步。那些想要在他们不须要阻塞线程的状况下使用这种方法的人能够选择经过将其做为委托传递来调用它Task.Run

任务返回“热”

C#中异步编程的一个方面并非很明显,即任务返回“热门”或已经开始。await关键字用于暂停代码,直到任务完成,但实际上并未启动它。当您同时查看对运行任务的影响时,这会变得很是有趣。

await FooAsync();
await BarAsync();
await BazAsync();

这里,三个任务串行运行。只有在FooAsync完成才会BarAsync启动,一样,BazAsync直到BarAsync完成才会启动这是因为正在等待内联任务。如今,请考虑如下代码:

var fooTask = FooAsync();
var barTask = BarAsync();
var bazTask = BazAsync();

await fooTask;
await barTask;
await bazTask;

在这里,任务如今并行运行这是由于这三个都是在全部三我的随后等待以前开始的,由于他们又回来了。

考虑到Task.WhenAll存在,这彷佛有点反直觉若是全部任务都已在运行,为何须要该功能?简单地说,Task.WhenAll做为一种等待完成一组任务的方式存在,以便在全部结果都准备好以前代码不会继续。

var factor1Task = GetFactor1Async();
var factor2Task = GetFactor2Task();

await Tasks.WhenAll(factor1Task, factor2Task);

var value = factor1Task.Result * factor2Task.Result;

因为两个任务都须要在咱们运行乘法线以前完成,所以咱们能够暂停,直到两个任务完成等待Task.WhenAll不然,它并不重要。事实上,Task.WhenAll若是您等待两个任务而不是Result直接调用,您甚至能够放弃

var value = (await factor1Task) * (await factor2Task);

不管多长时间,它真的只是一个品味问题而不是任何东西。尽管如此,重要的是要意识到任务当即开始,而不是等待它们的行为致使它们开始。相反,等待只是阻止代码继续前进,直到任务完成。

相关文章
相关标签/搜索