zl程序教程

您现在的位置是:首页 >  其他

当前栏目

异步编程

2023-02-25 18:18:48 时间

一、为什么要用异步编程      

         异步的优点:可以提升效率,节省时间

         注意:异步并不能使得单个请求的速度提升只是相较于同步可以处理更多的请求

二、async ,await 基本使用

“异步方法”:用async关键字修饰的方法

1) 异步方法的返回值一般是Task<T>,T是真正的返回值类型,Task<int>。惯例:异步方法名字以Async结尾。

2) 即使方法没有返回值,也最好把返回值声明为非泛型的Task。

3)调用泛型方法时,一般在方法前加上await关,这样拿到的返回值就是泛型指定的T类型;

4)异步方法的“传染性”:一个方法中如果有await调用,则这个方法也必须修饰为async

static async Task Main(string[] args)
{
     string fileName = "d:/1.txt";
     //   File.Delete(fileName);
     File.WriteAllTextAsync(fileName, "hello async");
     string s = await File.ReadAllTextAsync(fileName);
     Console.WriteLine(s);
}

三、编写异步方法

static  async Task  Main(string[] args)
{ //调用下载方法
int L=await  DownloadAsync ("https://www.qubcedu.com/" , @"d:/1.txt");
} 

static async Task<int> DownloadAsync(string url, string destFilePath)
{
   
  string body;
  
  using (HttpClient httpClient = new HttpClient())
    
  {
        
  body = await httpClient.GetStringAsync(url);
    //获取 url  的 html
    await File.WriteAllTextAsync(destFilePath, body);
    
   }
  
   
     return body.Length;
     
}

注意:

 如果同样的功能,既有同步方法,又有异步方法,那么首先使用异步方法。

.NET5中,很多框架中的方法也都支持异步:Main、WinForm事件处理函数。

对于不支持的异步方法怎么办?Wait()(无返回值);Result(有返回值)。

风险:死锁。尽量不用。

static void Main (string [] args)
{
//有返回值
//  string s=File.ReadAllTextAsync(@"e\1.txt").Result;
//无返回值
    string s=File.ReadAllTextAsync(@"e\1.txt","aaaaaa").Wait();
}

四、Async、await 原理揭秘

static async Task Main(string[] args)
{
    using (HttpClient httpClient = new HttpClient())
    {
   
         string html = await httpClient.GetStringAsync("https://www.taobao.com");
      
           Console.WriteLine(html);
   
            }
  
             string destFilePath = "d:/1.txt";
  
               string content = "hello async and await";
  
                 await File.WriteAllTextAsync(destFilePath, content);
 
                    string content2 = await File.ReadAllTextAsync(destFilePath);
    
                    Console.WriteLine(content2);
                    }

      用ILSpy反编译dll成C# 4.0版本,就能看到容易理解的底层IL代码

 await、async是“语法糖”,最终编译成“状态机调用”

总结:async的方法会被C#编译器编译成一个类,会主要根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。 用await看似是“等待”,经过编译后,其实没有“wait”。

(不懂得可以去反编译试一下,就用上面得这串代码就行)

五、Async 背后的线程切换

   await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,

   框架会从线程池再取出来一个线程执行后续的代码。

开始验证上述结论

Thread.CurrentThread.ManagedThreadId获得当前线程Id。

验证:在耗时异步(写入大字符串)操作前后分别打印线程Id

static async Task Main(string[] args)
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            StringBuilder s = new StringBuilder();
            for (int i = 0; i < 10000; i++)
            {
                s.Append("xxxxxxxxxxxxxxxxxxxxxx");
            }
            await File.WriteAllTextAsync(@"C:\Users\bug\Desktop\temp.txt",s.ToString());
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        }

结果输出:可以看到两次输出的线程id不相同

注意:如果写入内容少,会发现线程Id不变。

优化:到要等待的时候,如果发现已经执行结束了,那就没必要再切换线程了,剩下的代码就继续在之前的线程上继续执行了。

六、异步方法并不等于多线程

异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。

 static async Task Main(string[] args)
        {
            Console.WriteLine("之前" + Thread.CurrentThread.ManagedThreadId);
            double r = await  CalcAsync(5000);
            Console.WriteLine($"r={r}");
            Console.WriteLine("之后" + Thread.CurrentThread.ManagedThreadId);
        }
        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 = result + (double)rand.NextDouble();
            }
            return result;
        }

可以注意到现在虽然使用了异步方法,但是并没有切换线程;

此时添加一个线程再看效果

 static async Task Main(string[] args)
        {
            Console.WriteLine("之前" + Thread.CurrentThread.ManagedThreadId);
            double r = await  CalcAsync(5000);
            Console.WriteLine($"r={r}");
            Console.WriteLine("之后" + Thread.CurrentThread.ManagedThreadId);
        }
        static async Task<double> CalcAsync(int n)
        {
            return await Task.Run(() =>
            {
                Console.WriteLine("CalcAsync-ThreadId:" + Thread.CurrentThread.ManagedThreadId);
                double result = 1;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    result = result + (double)rand.NextDouble();
                }
                return result;
            });
          
        }

    }
}

结论:异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。

7、为什么有些异步方法没有标 async

有async

static async Task<string> ReadFileAsync(int num)
{
   
 switch (num)
    {
  
       case 1:
            return await File.ReadAllTextAsync("d:/1.txt");
    
           case 2:
            return await File.ReadAllTextAsync("d:/2.txt");
    
               default:
            throw new ArgumentException("num invalid");
 
                  }
                  }

async方法缺点:

1、异步方法会生成一个类,运行效率没有普通方法高;

2、可能会占用非常多的线程;

无async

static Task<string> ReadFileAsync(int num)
{
   
       switch (num)
    {
    
           case 1:
            return File.ReadAllTextAsync("d:/1.txt");
      
            case 2:
            return File.ReadAllTextAsync("d:/2.txt");
    
                default:
            throw new ArgumentException("num invalid");
 
                   }
}

只甩手Task,不“拆完了再装”

反编译上面的代码:只是普通的方法调用。

优点:运行效率更高,不会造成线程浪费。

返回值为Task的不一定都要标注async,标注async只是让我们可以更方便的await而已。

如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async关键字。

8、不要使用 sleep()

    sleep 不占用资源,delay线程等待占用cpu资源。

如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay()。

9、CancellationToken

        有时需要提前终止任务,比如:请求超时、用户取消请求。

        很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。

CancellationToken结构体

None:空

bool IsCancellationRequested 是否取消

(*)Register(Action callback) 注册取消监听

ThrowIfCancellationRequested() 如果任务被取消,执行到这句话就抛异常。

CancellationTokenSource

CancelAfter()超时后发出取消信号

Cancel() 发出取消信号

CancellationToken Token

10、WhenAll

Task类的重要方法:

1. Task<Task> WhenAny(IEnumerable<Task> tasks)等,任何一个Task完成,Task就完成

2. Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。

3. FromResult() 创建普通数值的Task对象。

Task<string> t1 = File.ReadAllTextAsync("d:/1.txt");
Task<string> t2 = File.ReadAllTextAsync("d:/2.txt");

Task<string> t3 = File.ReadAllTextAsync("d:/3.txt");

string[] results = await Task.WhenAll(t1, t2, t3);

string s1 = results[0];

string s2 = results[1];


string s3 = results[2];

11、异步其他问题

接口中的异步方法:

async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。

异步与yield:

复习: yield return不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。

static IEnumerable<string> Test()

{

yield return "hello";

yield return "xxk";

yield return "xxxx";

}

在旧版C#中,async方法中不能用yield。从C# 8.0 开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。

static async Task Main(string[] args)

{

await foreach(var s in Test())

{

Console.WriteLine(s);

}

}

static async IAsyncEnumerable<string> Test()

{

yield return "hello";

yield return "yzk";

yield return "youzack";

}