关于线程

默认情况下一个进程只包含一个线程。
线程可以派生其他线程,因此在任意时刻,一个进程可能包含不同状态的多个线程,它们执行程序的不同部分。
如果一个进程拥有多个线程,它们将共享进程的资源。
系统为处理器执行所调度的单元是线程,不是进程。

异步编程演示

异步获取百度的html然后异步写入test.txt文件中。

1
2
3
4
5
6
7
8
9
10
namespace Test {
internal class Program {
static async Task Main(string[] args) {
HttpClient client = new HttpClient();
string str = await client.GetStringAsync(@"https:\\www.baidu.com");
string fileName = @"F:\test.txt";
await File.WriteAllTextAsync(fileName,str);
}
}
}

异步方法在其完成所有工作之前就返回到调用方法。
主函数调用异步方法,然后主函数在异步方法执行时继续执行(可能在相同的线程也可能在不同的线程)
async:声明这种方法是异步的。
await:在异步方法内等待。

异步方法的注意点

异步方法头中用async修饰符,即使返回结果为空也应该将结果声明为Task.
异步方法包含一个或多个await。
异步方法必须返回void、Task、Task<T>、ValueTask<T>的其中之一。
异步方法应具有公开可访问的GetAwaiter方法类型。
异步方法名称一般以Async为后缀。
Lambda表达式和匿名方法也可以作为异步对象。

await

await操作符帮我们提供异步等待,并且可以将Task<T>所封装的真实值取出:

1
2
3
4
5
6
7
8
9
static async Task Main(string[] args) {
HttpClient client = new HttpClient();
//不用await帮忙,我就要自己取
Task<string> str1 = client.GetStringAsync(@"https:\\www.baidu.com");
await Console.Out.WriteLineAsync(str1.Result);
//await帮我取值
string str2 = await client.GetStringAsync(@"https:\\www.baidu.com");
await Console.Out.WriteLineAsync(str2);
}

await表达式制订了一个异步执行的任务,这个任务可能是Task也可能不是,默认情况下这个任务在当前线程上异步运行。
一个空闲对象既是一个awaitable类型的实例,awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个awaiter类型的对象。awaiter类型包含以下成员:
bool IsCompleted{get}
void OnCompleted(Action);
还包含以下成员之一:
void GetResult();
T GetResult();
实际上,你不需要构建自己的awaitable,相反,你应该使用Task或ValueTask类,它们是awaitable类型

Task.Run

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
internal class Program {   
static async Task Main(string[] args) {
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
await Repeat(100);
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
}
static async Task<int> Repeat(int times) {
int i = 0;
for(int j = 0; j < times; j++) {
i = i + j;
}
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
return i;
}
}

Task.Run()方法可以创建一个Task,并且在不同线程上运行你的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static async Task Main(string[] args) {
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
await Repeat(100);
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
}
static async Task<int> Repeat(int times) {
return await Task.Run(() => {
int i = 0;
for (int j = 0; j < times; j++) {
i = i + j;
}
Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);
return i;
});
}
}

它接受一个委托,委托类型可以是:

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

如果不需要传递异步结果,不需要包装成Task,就没必要async,在哪await都是await。
这样写对优化有帮助。

1
2
3
4
5
6
7
8
9
static async Task Main(string[] args) {
string str = await GetBaidu();
await Console.Out.WriteLineAsync(str);
}
static Task<string> GetBaidu() {
HttpClient client = new HttpClient();
return client.GetStringAsync("https://www.baidu.com");
}
}

取消异步操作CancellationToken和CancellationTokenSource

位于System.Threading.Tasks名称空间下。
很多异步方法都有CancellationToken参数用于获得提前终止执行的信号。
CancellationToken是一个结构体,它包含着一个任务是否应被取消的信息。
拥有CancellationToken对象的任务应定期检查其令牌状态也就是IsCancellationRequested属性是否为true,任务需停止其操作并返回。
CancellationToken是不可逆的只能使用一次。
CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象。任何持有CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationToken的IsCancellationRequested设置为true。

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
internal class Program {   
static async Task Main(string[] args) {
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;
cancellationTokenSource.CancelAfter(3000);
Task<int> res = GetBaidu(token);
while (true) {
if (Console.ReadLine() != "p") {
}
cancellationTokenSource.Cancel();
}
}
static async Task<int> GetBaidu(CancellationToken token) {
HttpClient client = new HttpClient();
for(int i = 0; i < 65533; i++) {
if(token.IsCancellationRequested) {
await Console.Out.WriteLineAsync("请求被取消");
return 1;
}
string str = await client.GetStringAsync("https://www.baidu.com");
await Console.Out.WriteLineAsync(str);
}
return 0;
}
}

同步等待任务

可以使用task对象的Wait()方法在调用方法中同步等待:

1
2
3
4
5
static async Task Main(string[] args) {
HttpClient client = new HttpClient();
Task<string> t = client.GetStringAsync("https://www.baidu.com");
t.Wait();
}

Task.WaitAll()静态方法用于等待一组任务都完成
Task.WaitAny()静态方法用于等待一组任务中的某一个完成

1
2
3
4
5
6
7
8
9
static async Task Main(string[] args) {
HttpClient client = new HttpClient();
Task<string> t1 = client.GetStringAsync("https://www.baidu.com");
Task t2 = File.ReadAllTextAsync(@"F:\test.txt");
//Task.WaitAll(t1, t2);
Task.WaitAny(t1, t2);
Console.WriteLine($"t1是否完成:{t1.IsCompleted},t2是否完成{t2.IsCompleted}");
}
}

它们的重载还有:

在异步方法中异步等待

有时在异步方法中,你会希望用await表达式来等待Task,这时异步方法会返回调用方法,但该异步方法会等待一个或所有任务完成。可以通过Task.WhenAll()和Task.WhenAny()来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
internal class Program {   
static async Task Main(string[] args) {
await Test();
}
static async Task Test() {
HttpClient client = new HttpClient();
Task<string> t1 = client.GetStringAsync("https://www.baidu.com");
Task t2 = File.ReadAllTextAsync(@"F:\test.txt");
//await Task.WhenAll(t1, t2);
await Task.WhenAny(t1, t2);
await Console.Out.WriteLineAsync($"t1是否完成:{t1.IsCompleted},t2是否完成{t2.IsCompleted}");
}
}

Task.Delay

该方法创建Task对象,将对象暂停在其线程中,一定时间后再完成,该方法不会阻塞主线程.

1
Task.Delay(3000);

GUI程序中的异步

1
2
3
4
5
6
7
private void Button_Click(object sender, RoutedEventArgs e) {
this.input.Text = "Watting";
this.btn.IsEnabled = false;
Thread.Sleep(4000);
this.input.Text = "没有任务";
this.btn.IsEnabled = true;
}

在wpf中当点击按钮,文本框文字没有改变,按钮也没有被禁用,但是页面卡住了三秒,期间无法拖动也无法进行其他活动。
原因是GUI需要处理很多事情,当点击按钮时,按钮的Click被放入消息队列,消息泵从队列中移除该消息并开始执行事件处理函数。事件处理函数将改变文本框文字、禁用按钮、移动窗体、改变文本框文字、启用按钮放入消息泵,消息泵还未刷新页面时,被Thread.Sleep()阻塞了主线程,当三秒之后再处理消息泵的内容,但是这一切发生的太快。
将上述代码修改如下即可:

1
2
3
4
5
6
7
private async void Button_Click(object sender, RoutedEventArgs e) {
this.input.Text = "Watting";
this.btn.IsEnabled = false;
await Task.Delay(4000);
this.input.Text = "没有任务";
this.btn.IsEnabled = true;
}

Task.Yield

该方法创建一个立即返回的awaitable。等待一个Yield可以让异步方法在执行后续部分的同时返回到调用方法,可以将其理解成离开当前的消息队列、回到队伍末尾、让处理器有时间处理其他任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
internal class Program {
static void Main(string[] args) {
Task<int> value = DoStuff.FindSeriesSum(100000);
CountBig(100000); CountBig(100000);
CountBig(100000); CountBig(100000);
Console.WriteLine($"Sum:{value.Result}");
}
private static void CountBig(int value) {
for (int i = 0; i < value; i++) { }
}
}
static class DoStuff {
public static async Task<int>FindSeriesSum(int i1) {
int sum = 0;
for(int i = 0; i < i1; i++) {
sum += i;
if(i%1000 ==0)await Task.Yield();
}
return sum;
}
}

Yield方法在GUI程序中非常有用,可以中断大量工作,让其他任务使用处理器。

异步Lambda表达式

1
Button.Click+=async(sender,e)=>{...};