2

如何避免让线程摸鱼,请用异步技术 async await 拿捏他~ - iDream

 1 year ago
source link: https://www.cnblogs.com/weixb/p/17104642.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

你点了外卖后,会一直不做其它事情,一直等外卖的到来么?
当然不会拉!

我们来看看代码世界的:

public void Query(){
    // 当前线程 向 数据库服务器 发起查询命令
    // 在 数据库服务器 返回数据之前,当前线程 一直等待,不干活了!!!
    var data = Database.Query(); 
}

假设在一个请求响应中:

  1. 线程用 5ms 来验证用户的输入的参数;
  2. 线程用 50ms 来等待数据库返回;
  3. 线程用 5ms 序列化数据响应返回给用户;

可以看到在 60ms 中,线程摸鱼 50ms。

而很多Web框架,收到一个请求,就会创建一个线程来处理,
如果片刻间内有100个用户请求这个方法,那么就得安排100个线程,
有没有方法让第1个线程在等待数据返回时,先去接待第N+1个用户(校验请求参数什么的)
这样就能大大减少线程数量~

通过上面的例子,我相信你已有所悟:异步就是避免让线程摸鱼。

概念与理论

接下来为了更有效地沟通和提示逼格,我们还是使用专业的术语。

复习一下线程的阻塞睡眠挂起
主要是弄明白阻塞的定义,和什么时候会发生阻塞

线程阻塞

Thread t = new Thread(()=>{
    // 阻塞:线程 被动 地等待外部返回,才能继续执行
    var resp = Http.Get(url); // 需要等待网络传输文档
});

线程睡眠

Thread t = new Thread(()=>{
    // 睡眠:线程 主动 停止执行片刻,然后继续执行
    Thread.Sleep(1000);
});

线程挂起

// 伪代码,C# 的 ThreadPool 没有这些方法

// 主动叫线程去休息
ThreadPool.Recycle(t)

// 等到有工作了,再叫线程处理执行
t = ThreadPool.GetThread();
t.Run(fun);

Synchronous(同步)
本人对 同步 给出比较容易理解的定义是:按顺序步骤,一个步骤只做一件事情。
本人以前看到 同步 这个词,错误地顾名思义,以为是同一刻时间做几件事,错错错!!!

// 线程会一步一步执行以下代码,这个过程叫 同步

// 先发完短信
SMS.Send(msg); // 2秒

// 再发邮件
Email.Send(smg); // 1秒

// 总耗时 3秒

Parallel(并行)
指两个或两个以上事件(或线程)在同一时刻发生。

// 分别创建两个线程并行去执行,谁也不用等待谁~
Thread t1 = new Thread(()=>{
    SMS.Send(msg); // 2秒
});

// t2 线程不需要等待 t1 线程
Thread t2 = new Thread(()=>{
    Email.Send(smg); // 1秒
});

// 总耗时 2秒

微软官方文档-使用 Async 和 Await 的异步编程

微软用的做早餐的例子:

  1. 倒一杯咖啡。
  2. 加热平底锅,然后煎两个鸡蛋。
  3. 煎三片培根。
  4. 烤两片面包。
  5. 在烤面包上加黄油和果酱。
  6. 倒一杯橙汁。

同步则是单人(单线程)从 1 到 6 一步一步地做 —— 效率低。
并行则是多人(多线程),一人倒咖啡;一人煎鸡蛋;一个...同时进行 —— 效率高,人力成本高。
异步则是单人(单线程),点火热平底锅,平底锅要等待变热,那么先把面包放进烤面包机...

Asynchronous(异步)
指的是,当线程遇到阻塞时,让线程先去执行其它工作~

我们应该体验过,当一个人要在很多事情上来回切换的时候,很容易出错。

做早餐,我们点火热平底锅后就去烤面包,但平底锅什么时候好,我们什么时候切换回来煎鸡蛋,还是去倒橙汁。

要将代码的执行过程写成异步的,也不是容易的事情。

好在 C# 提供 asyncawait 这两个关键字,
轻松创建异步方法(几乎与创建同步方法一样轻松) —— 微软官方文档原话

理论讲解完毕,是时候来实践了~

async 修饰符

public void Get()
{
    // 这是一个 同步方法
    // 如果这个内部有会发生阻塞的功能代码,比如读取网络资源,
    // 那么一个线程运行这个方法遇到阻塞,这个线程就会摸鱼~
}

要将一个同步方法声明为异步方法,首先需要将用 async 修饰符标记一下,

public async void Get()
{
    // 这是一个 异步方法
    // 如果这个内部有会发生阻塞的功能代码
    // 那么一个线程运行这个方法遇到阻塞时,这个线程就会去做其它事情~
}
public async void Get()
{
    HttpClient httpClient = new HttpClient();
    httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");
}

加入一些我们需要观察的代码后,得:

public static void Main()
{
    Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    Get();

    Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");


    Console.ReadKey();
}

// 这代码是有问题的,我有意为之,用来和接下来的更完善的代码做比较~
public static async void Get()
{
    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();
    httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");
}

运行后的控制台输出:

Main 开始执行前线程 Id:1
Get  开始执行前线程 Id:1
Get  执行结束后线程 Id:1
Main 执行结束后线程 Id:1

注意!!!这个时候方法虽然被声明为异步的,但现在执行过程还是同步的!!!!

await 运算符

微软官方文档:async(C# 参考) 中:

异步方法同步运行,直至到达其第一个 await 表达式,此时会将方法挂起,直到等待的任务完成。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行。 编译器警告将通知你不包含 await 语句的任何异步方法,因为该情况可能表示存在错误。 请参阅编译器警告(等级 1)CS4014。

所以完善的代码,应该是这样子的:

public static void Main()
{
    Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    Get(); // Get 方法虽然是声明为异步的,但依旧时同步执行

    Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");


    Console.ReadKey();
}

public static async void Get()
{
    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();

    // 加上 await 运算符,才是真正的异步执行!!!
    await httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");
}

运行后的控制台输出:

Main 开始执行前线程 Id:1 # 线程1,进入 main 函数 
Get  开始执行前线程 Id:1 # 线程1,执行 Get  函数,遇到阻塞,但线程1被要求不能摸鱼,
Main 执行结束后线程 Id:1 # 于是看看有没有其它工作做,发现需要打印...
Get  执行结束后线程 Id:9 # 阻塞结束后,谁来执行剩下的代码呢?
               # 如果线程1有空,可以回来执行,如果线程1忙,则有其它线程接管
               # 由调度分配决定

我们自己定义的异步方法 Get() 和调用异步方法 httpClient.GetAsync
只有 httpClient.GetAsync 是异步执行的。
也就是说单单使用 async 还不够,还得必须同时使用 await

Task 类

通常来说,我们使用 httpClient.GetAsync,都是希望能处理返回的数据。

微软官方文档:异步方法的返回类型

  • Task 表示不返回值且通常异步执行的单个操作。
  • Task<TResult> 表示返回值且通常异步执行的单个操作。
  • void 对于除事件处理程序以外的代码,通常不鼓励使用 async void 方法,因为调用方不能 await 那些方法,并且必须实现不同的机制来报告成功完成或错误条件。
public static async void Get()
{
    const string url = "https://learn.microsoft.com/zh-cn/docs/";

    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();
    // 用 Task 来 = 一个异步操作
    Task<HttpResponseMessage> taskResp = httpClient.GetAsync(url);

    HttpResponseMessage resp = await taskResp;// 等待异步操作完成返回
    // 可以对 resp 进行一些处理
    
    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");
}

上面代码可以简化为:

public static async void Get()
{
    const string url = "https://learn.microsoft.com/zh-cn/docs/";

    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();

    HttpResponseMessage resp = await httpClient.GetAsync(url);

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");
}

多个Task 的例子:

public static async void Get()
{
    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();

    var t1 = httpClient.GetAsync("https://learn.microsoft.com/");
    var t2 = httpClient.GetAsync("https://cn.bing.com/");
    var t3 = httpClient.GetAsync("https://www.cnblogs.com/");

    Console.WriteLine($"Get await 之前的线程 Id:{Thread.CurrentThread.ManagedThreadId}");
    
    await Task.WhenAll(t1, t2, t3); // 等待多个异步任务完成

    //Task.WaitAll(t1, t2, t3);
    //await Task.Yield();
    //await Task.Delay(0);

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");
}

运行后的控制台输出:

Main 开始执行前线程 Id:1
Get  开始执行前线程 Id:1
Get  await 之前的线程 Id:1
Main 执行结束后线程 Id:1
Get  执行结束后线程 Id:14

按微软官方文档的建议和规范的最终版本:

public static void Main()
{
    Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    GetAsync().Wait();

    Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");


    Console.ReadKey();
}

// 通常不鼓励使用 async void 方法
// 异步方法名约定以 Async 结尾
public static async Task GetAsync()
{
    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    HttpClient httpClient = new HttpClient();

    var t1 = httpClient.GetAsync("https://learn.microsoft.com/");
    var t2 = httpClient.GetAsync("https://cn.bing.com/");
    var t3 = httpClient.GetAsync("https://www.cnblogs.com/");

    Console.WriteLine($"Get await 之前的线程 Id:{Thread.CurrentThread.ManagedThreadId}");
    Task.WaitAll(t1, t2, t3); // 等待多个异步任务完成

    await Task.Yield();
    //await Task.Delay(0);

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");

}

运行后的控制台输出:

Main 开始执行前线程 Id:1
Get  开始执行前线程 Id:1
Get  await 之前的线程 Id:1
Get  执行结束后线程 Id:5
Main 执行结束后线程 Id:1
public static async Task GetAsync()
{
    Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    Stopwatch sw = new Stopwatch();
    sw.Start();

    TestHttp(); // http 网络不稳定,不好观察时间,可以试试 TestIdle()

    sw.Stop();
    Console.WriteLine($"一共耗时:{sw.ElapsedMilliseconds} 毫秒");

    Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}");

    await Task.Yield();
}

public static void TestHttp()
{
    HttpClient httpClient = new HttpClient();

    List<Task<HttpResponseMessage>> tasks = new List<Task<HttpResponseMessage>>();
    for (int i = 0; i < 10; i++)
    {
        var t = httpClient.GetAsync("https://learn.microsoft.com/");
        tasks.Add(t);
    }

    Task.WaitAll(tasks.ToArray());

    foreach (var item in tasks)
    {
        var html = item.Result.Content.ReadAsStringAsync().Result;
    }
}

public static void TestIdle()
{
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < 10; i++)
    {
        var t = Idle();
        tasks.Add(t);
    }

    Task.WaitAll(tasks.ToArray());
}

public static async Task Idle()
{
    // 可以用于模拟阻塞效果
    await Task.Delay(1000);

    // 不能用 Sleep 来模拟阻塞,Sleep 不是阻塞,是睡眠
    // Thread.Sleep(1000);
}
    
Main 开始执行前线程 Id:1
Get 开始执行前线程 Id:1
一共耗时:604 毫秒 # 1个线程干了10个线程的活,时间还差不多,美滋滋~
Get 执行结束后线程 Id:1
Main 执行结束后线程 Id:1

至此,关于 C# 中异步编程的三个知识点 asyncawaitTask 讲解完毕。

在写例子的过程中,
发现 HttpClient 这个类很多方法都是异步方法了,
依稀记得以前还有同步方法和异步方法提供选择的,
看来微软是在逼大家进步啊~

如果文章能帮到你,点个赞吧,十分感谢~

异步编程:
https://docs.microsoft.com/zh-cn/dotnet/csharp/async

使用 Async 和 Await 的异步编程:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async

异步编程模型:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

深入了解异步:
https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth

async 关键字:
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async

await 运算符:
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/await

Async/Await 异步编程中的最佳做法:
https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

Future 与 promise:
https://zh.wikipedia.org/wiki/Future与promise


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK