C#中的异步编程简介

174 阅读6分钟

你可能已经读过关于异步编程的文章。 asyncawait字随处可见,无论我们选择哪种编程语言。谈到.NET框架,特别是C#,我们有一些本地函数、类和保留词,我们可以用它们来实现我们项目中的异步任务和工作流。

在这篇文章中,我们将谈论同步性、并行性、并发性,以及如何在我们的C#应用程序中实现异步算法。

同步和异步任务

作为一个开发者,你肯定会面临某些行动或操作需要相当长的时间来执行的情况。我们经常要做一些长期运行的任务,如读取文件、调用API或下载一个大文件。我们可以等其中一个任务完成后再执行另一个任务。如果是这样的话,我们就说我们在以 "同步 "的方式工作。通过这样做,整个应用程序会被阻塞并停止响应,直到整个任务完成,我们就可以转到一个新的任务。

在某些情况下,我们没有任何选择。如果我们有任务1和任务2依赖于第一个动作的结果,我们将不得不等到任务1执行完毕后再启动任务2。但我们可能会有这样的情况:后续任务(或其中一些)并不依赖于前一个长期运行的任务的结果。如果是这种情况,我们可以采取不同的策略和方法,使我们的应用程序更快、更有性能。

例如,我们可以有一个内部运行并发任务的应用程序。有一个按钮,当它被点击时有一个任务被执行。在用户点击按钮后,应用程序可以触发一个单独的线程来运行所要求的任务。同时,主线程可用于执行其他动作,而按钮的任务则在后台执行。这样做,我们可以保持UI的响应性,以防用户想与它互动。

另一种情况是需要运行某组动作或指令的多个副本。这方面的一个例子是同时上传许多文件。在这种情况下,我们的应用程序可以为每个文件触发一个线程并在其中执行必要的代码。这样做,我们将以一种 "并行 "的方式处理这些文件。简而言之,这里是这两个概念之间的区别:并发性意味着应用程序同时在一个以上的任务上取得进展,而并行性是指同时运行多个任务。

但是,假设我们要读取一个大文件,调用一个API,并做一些复杂的计算。这三个任务之间没有依赖关系,但我们需要所有任务的结果来继续执行我们的应用程序并更新用户界面。在这种情况下,我们可以 "异步 "地执行我们的任务,同时运行这三个任务,并等待它们的结果来完成后续的任务。

什么是异步编程?

我们可以将异步编程定义为在一个线程中执行编程代码的方式,而不需要等待与I/O相关的或与CPU相关的任务完成。I/O绑定的操作可以是文件系统访问、HTTP请求、API调用或数据库查询。与CPU相关的操作可以是加密数据、复杂计算、图像或文件管理等操作。

异步编程的理念之一是将我们的逻辑划分为可等待的任务,这样我们就不会阻碍我们应用程序的执行。我们可以调用一个异步方法并获得一个代表它的任务对象。在此期间,我们可以做一些不相关、不依赖的工作。在我们执行这些操作之后,我们等待异步任务,这个任务可能已经完成,也可能没有完成。如果执行完毕,我们将从任务中获得结果值,并将其用于下一个相关的操作。

优点

我们使用异步任务的一些好处是。

  • 我们保持我们的应用程序的用户界面的响应性。
  • 我们提高了我们的应用程序的性能。
  • 我们避免了线程池的饥饿

缺点

虽然,在使用异步编程时,也有一些缺点。

  • 代码变得更复杂,更难维护。
  • 内存分配增加,因为一些对象在等待其他代码执行时必须保持更长时间的活力。
  • 很难发现异步任务中出现的错误。
  • 当我们在写一段异步代码时,我们所有的应用代码都会变成异步的。

异步编程模式

为了在.NET中执行异步操作,我们可以遵循三种不同的模式。

异步编程模式(APM):假设我们有两个方法,我们把它们命名为BeginOperationEndOperation 。在调用BeginOperation ,我们的应用程序可以继续在调用线程上执行任务,而异步任务则在不同的线程上执行。对于每一次对BeginOperation 的调用,我们的应用程序还应该调用EndOperation 方法来获得结果。在.NET中,可以使用IAsyncResult 来实现。让我们看看这个模式的一个例子。

using System;
using System.IO;
using System.Threading;
public sealed class Program
{
    public static void Main()
    {
        var buffer = new byte[100];
        var fs = new FileStream("bigFile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

        IAsyncResult result = fs.BeginRead(buffer, 0, buffer.Length, null, null);

        // Do other things...

        int numBytes = fs.EndRead(result);
        fs.Close();

        Console.WriteLine("Read {0}  Bytes:", numBytes);
    }
}

基于事件的异步模式(EAP):我们启动一个异步方法,当任务完成后会触发一个Completed 事件,让我们的应用程序获得结果。这将是这种模式的一个例子。

public class ExampleHandler
{
    public event EventHandler OnTriggerCompleted;

    public void Start(int timeout)
    {
        var timer = new Timer(new TimerCallback((state) =>
        {
            OnTriggerCompleted?.Invoke(null, null);
        }));

        timer.Change(timeout, 0);
    }
}

class Program
{
    private static void Main()
    {
        var handler = new ExampleHandler();

        handler.OnTriggerCompleted += (sender, e) =>
        {
            Console.WriteLine($"Triggered at: { DateTime.Now.ToLongTimeString()}");
        };

        handler.Start(3000);

        Console.WriteLine($"Start waiting at {DateTime.Now.ToLongTimeString()}");
        Console.WriteLine($"Processing...");
        Console.ReadLine();
    }
}

基于任务的异步模式(TAP):我们有一个OperationAsync 方法,返回一个Task 对象,就像下面的例子一样。

class ClassName
{
    public Task OperationAsync(byte [] buffer, int offset, int count);
}

我们可以等待该方法,使用 asyncawait关键字。我们将在一段时间内更深入地了解这种方法。

请记住,APM和EAP方法是传统的模式,它们不再被推荐。微软建议使用基于任务的异步模式来实现我们应用程序中的异步编程。

C#中的异步编程

正如我们提到的,C#中的异步编程可以通过实现基于任务的异步模式来完成。我们将有一些方法返回一个TaskTask<T>对象。将这些方法定义为异步操作将使我们能够等待它们,并继续使用同一个执行线程来运行其他与等待的任务无关的操作。

C#为我们提供了两个关键字,以更简单的方式处理Task 对象。 asyncawait.将关键字 async到方法签名中,允许我们在方法中使用 await关键字,同时指示编译器创建一个状态机来处理非同步性。另一方面,该 await关键字用于暂停一个方法的执行,异步地等待一个Task ,同时将当前线程送回线程池,而不是将其保持在阻塞状态。一切都发生在后台,避免了我们实现和维护线程管理的复杂性和调用的状态。

让我们分析一下这段代码。

public async Task<User> GetLoggedUserEmailAsync()
{
  int userId = GetId();
  string email = await GetEmailAsync(userId);
  User user = GetUserByEmail(email);
  return user;
}

public async Task<string> GetEmailAsync(int userId)
{
  // Do something
}

一个异步方法应该返回 void,Task, 或 Task<T>,其中 T是我们需要的返回数据类型。返回 void通常用于事件处理程序。关键字 async使我们能够在方法中使用命令 await方法内,这样我们就可以按照预期等待异步方法的处理。

请注意,这些方法以 "Async "结尾。虽然这不是强制性的,但有一个命名惯例,即异步方法的名称应该以 "Async "结束。这个约定的目的是让消费者清楚地知道,这个方法不会同步完成所有的工作。

在我们前面的例子中。 GetId()被同步调用。当执行线程遇到 await关键字时 await GetEmailAsync(userId)时,它会创建一个 Task<User>该任务包含GetLoggedUserEmailAsync 方法的剩余部分。这个任务是异步执行的,在 Task<string>``GetEmailAsync 返回。因此,User 对象从 Task<User>创建的 await关键字返回。

行动中的异步代码

让我们看一个简短的例子来回顾之前解释的概念。拥有以下的控制台应用程序。

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
   static async Task Main(string[] args)
   {
       string filePath = "bigFile.txt";

       // Create a big file
       FileStream fs = new FileStream(filePath, FileMode.CreateNew);
       fs.Seek(1024 * 1024, SeekOrigin.Begin);
       fs.WriteByte(0);
       fs.Close();

       var task = ReadFileAsync(filePath);

       Console.WriteLine("A synchronous message");

       int length = await task;

       Console.WriteLine("Total file length: " + length);
       Console.WriteLine("After reading message");
       Console.ReadLine();
   }

   static async Task<int> ReadFileAsync(string file)
   {
       Console.WriteLine("Start reading file");

       int length = 0;

       using(StreamReader reader = new StreamReader(file))
       {
           string fileContent = await reader.ReadToEndAsync();
           length = fileContent.Length;
       }

       Console.WriteLine("Finished reading file");

       return length;
   }
}

在这个应用程序中,我们读取一个大文件,计算它的字符数,并在控制台打印不同的信息。触发文件读取操作的方法被定义为异步的,而且是启动读取文本的异步线程。当 "一个同步消息 "被打印出来时,文件的读取仍在继续。如果我们执行该应用程序,我们可以看到执行线程是如何基于输出消息的行为的。

Asynchronous sample application output

总结

在这篇文章中,我们谈到了如何根据依赖性和执行顺序来管理和组织我们应用程序中的任务。我们谈到了同步性、并行性、并发性和异步性。我们描述了异步编程,它的好处,以及我们如何在我们的C#应用程序中实现它。你可以在这个GitHub资源库中找到本文中的代码。

如果你想了解更多关于C#中的异步编程以及我们有哪些高级功能,你可以去看看。

除此以外。用Auth0保证ASP.NET Core的安全

用Auth0保证ASP.NET Core应用程序的安全是很容易的,并带来了很多伟大的功能。有了Auth0,你只需要写几行代码就可以得到一个坚实的身份管理解决方案单点登录、对社会身份提供者(如Facebook、GitHub、Twitter等)的支持,以及对企业身份提供者(如活动目录、LDAP、SAML、自定义等)的支持。

在ASP.NET Core上,你需要在你的Auth0管理仪表板上创建一个API,并在你的代码上改变一些东西。要创建一个API,你需要注册一个免费的Auth0账户。之后,你需要进入仪表板的API部分,点击 "创建API"。在显示的对话框中,你可以将你的API的名称设置为 "书籍",标识符"http://books.mycompany.com",并将签名算法设为 "RS256"。

Creating API on Auth0

之后,你必须将调用 services.AddAuthentication()方法中的 ConfigureServices()``Startup 的方法,如下所示。

string authority = $"https://{Configuration["Auth0:Domain"]}/";
string audience = Configuration["Auth0:Audience"];

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
  options.Authority = authority;
  options.Audience = audience;
});

Configure()``Startup 方法的主体中,你还需要添加一个调用到 app.UseAuthentication()app.UseAuthorization()如下图所示。

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

请确保你按照上面的顺序调用这些方法。这是至关重要的,这样一切才能正常工作。

最后,将以下元素添加到 appsettings.json配置文件。

{
  "Logging": {
    // ...
  },
  "Auth0": {
    "Domain": "YOUR_DOMAIN",
    "Audience": "YOUR_AUDIENCE"
  }
}

:替换占位符 YOUR_DOMAINYOUR_AUDIENCE替换为你在创建Auth0账户时指定的域名和你分配给你的API的标识符的实际值。