02020205 .NET Core重点知识-05 异步方法不等于多线程、为什么有的异步方法没有声明async、异步方法不要用Sleep()

02020205 .NET Core重点知识-05 异步方法不等于多线程、为什么有的异步方法没有声明async、异步方法不要用Sleep()

1. 异步方法不等于多线程(视频Part2-8)

  • 异步方法并不会自动在新线程中执行,除非手动把代码放到新线程中执行。
1.1 异步方法在同一线程中执行
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("调用异步方法前的ThreadId:" + Thread.CurrentThread.ManagedThreadId);
            double d = await CalcAsync(500);
            Console.WriteLine($"{d}");
            Console.WriteLine("调用异步方法后的ThreadId:" + Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }

        public static async Task<double> CalcAsync(int n)
        {
            Console.WriteLine("CalcAsync.ThreadId:" + Thread.CurrentThread.ManagedThreadId);
            double result = 1;
            Random rand = new Random();
            for (int i = 0; i < n * n; i++)
            {
                result += rand.NextDouble();
            }
            return result;
        }
    }
}

控制台输出:
调用异步方法前的ThreadId:1
CalcAsync.ThreadId:1
125099.27453887419
调用异步方法后的ThreadId:1

说明:上述线程没有变化。因为上述代码,任然是在Main方法所在的线程里面执行。
1.2 异步方法在多线程中执行
  • 把要执行的代码以委托的形式传递给Task.Run方法,这样就会从线程池中取出一个线程执行我们的委托。
await Task.Run(() => { ...耗时操作代码,可以return返回值...});

说明:Run方法有很多重载,有些有返回值,有些没有返回值。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("调用异步方法前的ThreadId:" + Thread.CurrentThread.ManagedThreadId);
            double d = await CalcAsync(500);
            Console.WriteLine($"{d}");
            Console.WriteLine("调用异步方法后的ThreadId:" + Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }

        public static async Task<double> CalcAsync(int n)
        {
            return await Task.Run(() => // @2 这Run方法返回值的类型的是在@1处返回值的基础上推断出来的类型。
            {
                Console.WriteLine("CalcAsync.ThreadId:" + Thread.CurrentThread.ManagedThreadId);
                double result = 1;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    result += rand.NextDouble();
                }
                return result; // @1 返回值为double
            });
        }
    }
}

控制台输出:
调用异步方法前的ThreadId:1
CalcAsync.ThreadId:4
125140.19553880369
调用异步方法后的ThreadId:4

说明:
1. 线程Id变了。
2. 把一段耗时代码通过Task.Run扔到线程池里面,可以从线程池里面取出一个新的线程来执行后续代码。
1.3 总结
  • 通过对比1.1和1.2中的代码,可以得出如下结论。
    • 异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。
    • 可以通过Task.Run方法是实现多线程的方式之一。

2. 为什么有的异步方法没有声明async(视频Part2-9)

2.1 C#中内置未声明async的异步方法
public static Task<string> ReadAllTextAsync(string path, ...) // 方法声明并没有async,但它仍然是一个异步方法。
2.2 自定义没有asycn的异步方法
// 先创建两个.txt文件。
// @1 用传统的异步方法return文件中的内容。
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string st01 = await ReadAsync(1);
            Console.WriteLine(st01);


            Console.ReadLine();
        }
        
        static async Task<string> ReadAsync(int num)
        {
            if(num == 1)
            {
                string s = await File.ReadAllTextAsync(@"e:\01.txt");
                return s;
            }
            if (num == 2)
            {
                string s = await File.ReadAllTextAsync(@"e:\02.txt");
                return s;
            }
            else
            {
                throw new ArgumentException();
            }
        }
    }
}

控制台输出:
测试01
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// @2 不写async,不写await,直接return文件中的内容。
using System;
using System.IO;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string st01 = await ReadAsync(1); // @2.4 使用await,将Task<string>类型的返回值变为string类型。
            Console.WriteLine(st01);


            Console.ReadLine();
        }
        
        static Task<string> ReadAsync(int num) // @2.1 不写async声明,返回Task<string>。
        {
            if(num == 1)
            {
                return File.ReadAllTextAsync(@"e:\01.txt"); // @2.2 不用await,返回的是Task<string>
            }
            if (num == 2)
            {
                return File.ReadAllTextAsync(@"e:\02.txt"); // @2.3 不用await,返回的是Task<string>
            }
            else
            {
                throw new ArgumentException();
            }
        }
    }
}

控制台输出:
测试01

说明:
1. 在@2.1,@2.2,@2.3处写法,符合C#的语法规范,因为ReadAllTextAsync返回值类型就是Task<string>,方法声明的返回值也是Task<string>。
2. 在@2.4处,使用await调用方法,将返回值拿到,也没问题。因为这里用了await,那么Main方法也应该为async,都符合语法规范。
3. 在@2.2处和@2.3出没用await,那么在@2.1处也不能用async声明。如果写了,反而不对。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:
1. 异步方法会生成一个类,运行效率没有普通方法高。
2. 异步方法可能会占用非常多的线程。
3. 上述代码段在@1处用的是async声明的异步方法,而在@2处并没有使用异步方法。
4. 采用@2处的形式更加推荐使用。只甩手Task,不“拆完了再装”,反编译上面的代码,可以发现只是普通的方法调用。不用每层都用await,造成线程切换。
5. 采用@2处的形式,运行效率更高,不会造成线程浪费。
2.3 注意事项
  • 返回值为Task的不一定都要标注async,标注async知识让我们更方面的await而已。
  • 如果一个异步方法只是对别的异步方法调用的触发,并没有太多复杂的逻辑。
    • 比如等待A的结果,再调用B;把A的调用的返回值拿到B的内部做一些处理再返回。此时,就可以去掉async关键字。
2.4 将1.2中的async去掉
// 形式1 老师讲解
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("调用异步方法前的ThreadId:" + Thread.CurrentThread.ManagedThreadId);
            double d = await CalcAsync(500);
            Console.WriteLine($"{d}");
            Console.WriteLine("调用异步方法后的ThreadId:" + Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }

        public static Task<double> CalcAsync(int n) // @1 去掉async
        {
            return Task.Run(() => // @2 不再用await拆
            {
                Console.WriteLine("CalcAsync.ThreadId:" + Thread.CurrentThread.ManagedThreadId);
                double result = 1;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    result += rand.NextDouble();
                }
                return Task.FromResult(result); // @3 手动的将double包装成Task返回
            });
        }
    }
}

控制台输出:
调用异步方法前的ThreadId:1
CalcAsync.ThreadId:4
125140.19553880369
调用异步方法后的ThreadId:4
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 形式2 老师没讲
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo02
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("调用异步方法前的ThreadId:" + Thread.CurrentThread.ManagedThreadId);
            double d = await CalcAsync(500);
            Console.WriteLine($"{d}");
            Console.WriteLine("调用异步方法后的ThreadId:" + Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }

        public static Task<double> CalcAsync(int n) // @1 去掉async
        {
            return Task.Run(() => // @2 不再用await拆
            {
                Console.WriteLine("CalcAsync.ThreadId:" + Thread.CurrentThread.ManagedThreadId);
                double result = 1;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    result += rand.NextDouble();
                }
                return result; // @3 这里任然返回double类型的result
            });
        }
    }
}
控制台输出:
调用异步方法前的ThreadId:1
CalcAsync.ThreadId:4
125140.19553880369
调用异步方法后的ThreadId:4

说明:方式1和方式2在@3处,返回的类型不同,但是都没有报错,程序执行结果也相同。推测Task.Run方法可以自动完成对double类型的包装。

3. 不要用sleep方法(Part2-10)

  • 如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay()。
// 下载一个网站,3S后下载另一个网站。

老师课上用WinForm项目举例,在次不做代码笔记,只给结论。

说明:
1. 异步方法中暂停不用Thread.Sleep(),而是用await Task.Delay()。在控制台中看不到直接区别,但是放到WinForm程序中就能看到区别。
2. ASP.NET Core中也看不到直接的区别,但是Sleep()会降低并发,用点餐举例子如下。
2.1 用Sleep,那么所有的服务员都停在那里不能动。
2.2 用Delay,那么只是我在这里翻看菜单,所有的服务员还是在正常服务。

4. 作业

  • 封装一个异步方法,下载给定的网址,如果下载失败,则稍等500ms再重试。如果重试三次仍然失败,则抛出异常“下载失败”。

结尾

书籍:ASP.NET Core技术内幕与项目实战

视频:https://www.bilibili.com/video/BV1pK41137He

著:杨中科

ISBN:978-7-115-58657-5

版次:第1版

发行:人民邮电出版社

※敬请购买正版书籍,侵删请联系85863947@qq.com※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-08-25 20:01  qinway  阅读(7)  评论(0)    收藏  举报