为什么要使用异步编程

性能优化:

主线程利用率:异步编程允许主线程持续运行多个任务,避免因子线程阻塞而降低性能。
I/O密集型任务处理:异步编程特别适合处理大量I/O操作的任务,如文件读写、数据库查询等,这些操作可以独立于主线程运行,提升应用响应速度。
可扩展性:

多处理器支持:异步编程能够充分利用多处理器资源,每个CPU核同时处理不同的任务,从而提高系统的性能和稳定性。
高负载请求处理:在处理大量并发请求时,同步编程可能导致主线程被阻塞,而异步编程则能够有效地分担压力。
现代Web开发趋势:

技术兼容性:与主流Web框架(如React、Vue)的集成更加顺畅,使用async/await关键字可以简化代码,提高开发效率。
用户体验提升:异步编程通过延迟加载等技术减少用户等待时间,提升应用体验。
资源利用效率:

多任务并行处理:主线程可以通过多线程或协 ordinates 执行多个任务,避免资源浪费,提高系统的吞吐量。
趋势适应性:

异步编程的流行:在现代应用开发中,异步编程因其简洁性和高效性而备受青睐,采用该模式可以与现有技术栈更好地结合。

创建异步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
static void Main(string[] args)
{
await DownloadHTmlAsync("http://www.ptpress.com.cn", @"E:\temp\1.txt");
}
static async Task DownloadHTmlAsync(string url,string filename)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(filename, html);
}
}

带返回值的
获取长度

1
2
3
4
5
6
7
8
9
10
11
12
13
static async Task Main(string[] args)
{
int I = await DownloadHTmlAsync("http://www.ptpress.com.cn", @"E:\temp\1.txt");
}
static async Task<int> DownloadHTmlAsync(string url,string filename)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(filename, html);
return html.Length;
}
}

HttpClient: 提供一个类,用于从 URI 标识的资源发送 HTTP 请求和接收 HTTP 响应。
Flie: 提供用于创建、复制、删除、移动和打开单个文件的静态方法,并有助于创建 FileStream 对象。
GetStringAsync: 将 GET 请求发送到指定 URI 并在异步操作中以字符串的形式返回响应正文。

创建一个自定义的异步任务类:
创建一个继承自Task< T >的类,并在方法中添加[System.Threading.Await]或使用async关键字。

下载HTML内容:
在异步方法中,使用HttpClient来获取指定URL的HTML内容。这一步是非阻塞操作,不会阻塞主线程。

将HTML内容保存到文件:
使用File.WriteAllTextAsync将获取到的HTML内容写入指定的目标文件路径,并确保资源被正确关闭。

async 线程切换

理解关键字“async”与“await”:

async用于定义一个可以执行异步操作的任务。
await用于等待该任务的完成。
线程切换的基本概念:

线程切换是程序运行时从一个线程切换到另一个线程的过程。
使用“async”和“await”,C#在主线程和异步任务之间自动进行线程切换。
await触发的线程切换过程:

当调用带有async关键字的任务,并使用await等待其完成时,C#会暂停主线程。
创建一个新的后台线程来执行该异步任务。
直到该后台线程完成或抛出异常,主线程才会重新启动。
嵌套使用“await”的情况:

在一个async任务内部再次调用另一个带有async和await的任务时,C#会创建新的线程并切换主从线程。
这种多层嵌套增强了代码的可读性和灵活性。
同步与错误处理:

await操作会阻塞主线程直到所有等待的任务完成或出现异常。
如果某个任务抛出异常,后续的所有await操作都会终止,并按照错误处理机制进行。
潜在的问题与优化:

当多个任务需要阻塞主线程时,可能会导致主线程被频繁切换,影响性能。这种情况下,可以考虑使用异步队列或其他结构来提高效率。
使用适当的同步和错误处理机制可以避免资源泄漏和其他潜在问题。
总结:

“async”与“await”通过自动管理线程切换,简化了编写异步操作的代码,提高了开发效率。
正确理解和使用这些关键字能够有效提升程序的性能和可维护性。

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码(不一定时同一个线程池)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
static async Task Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder sb= new StringBuilder();
for(int i = 0; i < 10000; i++)
{
sb.Append("youxianyu");
}
await File.WriteAllTextAsync(@"E:\temp\1.txt", sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

1
2
1
8

总结:编译器把async拆分成多次方法调用,程序在运行的时候会通过线程池中取出空闲线程执行不同MoveNext调用的方法来避免线程的“空等”,从而提升系统的并发处理能力。

异步方法不等于多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static async Task Main(string[] args)
{
Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId);
double r=await CalcAsync(500);
Console.WriteLine($"r={r}");
Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId);
}
public static async Task<double> CalcAsync(int n)
{

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static async Task Main(string[] args)
{
Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId);
double r=await CalcAsync(500);
Console.WriteLine($"r={r}");
Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId);
}
public static async Task<double> CalcAsync(int n)
{
return await Task.Run(() =>
{
Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId);
double result = 0;
Random rand = new Random();
for (int i = 0; i < n * n; i++)
{
result += rand.NextDouble();
}
return result;
});

可以看到Task.Run的线程ID和Main方法中的线程ID不同,这说明Task.Run中的代码被放到新线程中执行了。

为什么有的异步方法没标async

有async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static async Task Main(string[] args)
{
string s1=await ReadFileAsync(1);
Console.WriteLine(s1);
}
static async Task<string> ReadFileAsync(int num)
{
switch (num)
{
case 1:
return await File.ReadAllTextAsync("e:/temp/1.txt");
case 2:
return await File.ReadAllTextAsync("e:/temp/2.txt");
default:
throw new ArgumentException("num参数不正确");
}
}

没有async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

static Task Main(string[] args)
{
string s1=await ReadFileAsync(1);
Console.WriteLine(s1);
}
static Task<string> ReadFileAsync(int num)
{
switch (num)
{
case 1:
return File.ReadAllTextAsync("e:/temp/1.txt");
case 2:
return File.ReadAllTextAsync("e:/temp/2.txt");
default:
throw new ArgumentException("num参数不正确");
}
}

区别:
由于ReadFileAsync方法没有用async关键字修饰,因此我们没有看到ReadFileAsync方法的代码编译生成的类,ReadFileAsync内部也不像async方法那样复杂,而只是简单地调用File.ReadAllTextAsync方法。ReadFileAsync方法没有编译生成类,因此不会增加程序集的尺寸,而且运行效率更高。因此,如果一个异步方法只是对别的异步方法进行简单的调用,并没有太多复杂的逻辑,比如获取异步方法的返回值后再做进一步的处理,就可以去掉async、await关键字。

不要用sleep

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    static async Task Main(string[] args)
{
int sum = await ReadFileAsync("http://www.baidu.com");
Console.WriteLine(sum);
}
static public async Task<string> ReadFileAsync(string url, int init=0)
{
try
{
using (var httpClient = new HttpClient())
{
var html = await httpClient.GetStringAsync(url);
return html;
}
}
catch (Exception e)
{
init++;
if (init >= 3)
{
throw new Exception("请求失败");
}
await Task.Delay(5000);
await ReadFileAsync(url, init);

return "下载失败";
}

}

}

用于获得提前终止执行的信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(3000);
CancellationToken cToken= cts.Token;
await DownLoadAsync("http://www.baidu.com", 10,cts.Token);
}
static async Task DownLoadAsync(string url, int n, CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("请求被取消");
break;
}
}
}
}

CancellationTokenSource:向 CancellationToken 发出应取消的信号。
CancelAfter:在此 CancellationTokenSource 上计划取消操作。
CancellationToken:传播应取消作的通知。
IsCancellationRequested:获取是否已为此令牌请求取消。

whenAll

创建一个任务,该任务将在所有提供的任务完成时完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static async Task Main(string[] args)
{
//Task<string> t1= File.ReadAllTextAsync(@"E:\temp\1.txt");
//Task<string> t2 = File.ReadAllTextAsync(@"E:\temp\2.txt");

//string[] strs = await Task.WhenAll(t1, t2);
//string a1= strs[0];
//string a2 = strs[1];
//Console.WriteLine(a1);
//Console.WriteLine(a2);
string[] files=Directory.GetFiles(@"E:\temp");
Task<int>[] countTasks=new Task<int>[files.Length];
for(int i = 0; i < files.Length; i++)
{
string filename = files[i];
Task<int> t= ReadCharsCount(filename);
countTasks[i] = t;
}
int[] counts=await Task.WhenAll(countTasks);
int c=counts.Sum();//求和
Console.WriteLine(c);
}
static async Task<int> ReadCharsCount(string filename)
{
string s=await File.ReadAllTextAsync(filename);
return s.Length;
}

GetFiles:返回满足指定条件的文件的名称。
Directory:公开用于通过目录和子目录进行创建、移动和枚举的静态方法。 此类不能被继承。