C#多线程之异步线程🔥
在 C# 中,async 和 await 是用于异步编程的关键字。它们允许你编写异步代码,可以用于执行耗时的操作(如文件 I/O、网络请求、数据库查询等),而不会导致 UI 界面被冻结,不会阻塞当前线程,从而提高程序的性能和响应性。
await 关键字用于等待异步操作完成。在使用 await 关键字时,方法会暂时挂起,直到等待的异步操作完成,然后继续执行后续代码。await 只能在 async 方法中使用。
# 使用
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
Program p = new Program();
p.StartTest().Wait();
}
public async Task StartTest()l
{
await AsyncTest();
}
private async Task AsyncTest()
{
Console.WriteLine($"AsyncTest内部【{Thread.CurrentThread.ManagedThreadId}】");
int n = 0;
for (int i = 0; i < 10000; i++)
n++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 返回类型
async 关键字用于声明一个方法是异步的。在异步方法中,可以使用 await 关键字来等待异步操作的完成。异步方法有一下三种返回类型: 如果异步方法不返回任何值,则返回类型应为Task。例如:
public async Task DoSomethingAsync()
{
return Task.Delay(1000);
}
2
3
4
如果异步方法返回一个值,则返回类型应为
Task<TResult>
其中 TResult 是返回值的类型。例如:
public async Task<int> GetResultAsync()
{
await DoSomethingAsync();
return 1;
}
2
3
4
5
最后一种是返回void,多用于事件触发器等特殊情况,例如,在 Windows Forms 或 WPF 应用程序中,处理按钮点击事件的方法通常是 void,因为事件处理器不能返回 Task。
private async void Button_Click(object sender, EventArgs e)
{
// 异步操作
await SomeAsyncOperation();
}
2
3
4
5
# 死锁
异步死锁通常发生在使用了 async 和 await 的情况下,其中一个异步操作等待另一个异步操作完成,而另一个异步操作又依赖于前者的完成,导致两个操作相互等待,最终导致死锁。以下是几个简单的异步死锁例子:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Start");
// 异步方法1
async Task Method1()
{
Console.WriteLine("Method1 start");
// 调用异步方法2并等待其完成
await Method2();
Console.WriteLine("Method1 end");
}
// 异步方法2
async Task Method2()
{
Console.WriteLine("Method2 start");
// 调用异步方法1并等待其完成
await Method1();
Console.WriteLine("Method2 end");
}
// 启动异步方法1
await Method1();
Console.WriteLine("End");
}
}
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
33
在上面的例子中,Method1 和 Method2 是两个异步方法。Method1 中调用了 Method2 并等待其完成,而 Method2 中又调用了 Method1 并等待其完成。这样,当程序执行到 await Method1(); 和 await Method2(); 时,两个异步方法相互等待对方的完成,导致了死锁。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Start");
// 异步方法1
async Task Method1()
{
Console.WriteLine("Method1 start");
// 模拟耗时的异步操作
await Task.Delay(100);
Console.WriteLine("Method1 end");
}
// 异步方法2
async Task Method2()
{
Console.WriteLine("Method2 start");
// 模拟耗时的异步操作
await Task.Delay(100);
Console.WriteLine("Method2 end");
}
// 在异步上下文中等待两个方法的完成
await Task.WhenAll(Method1(), Method2());
Console.WriteLine("End");
}
}
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
33
在这个例子中,Main 方法等待两个异步方法 Method1 和 Method2 的完成。这两个方法都包含了一个耗时的异步操作,但是由于它们在同一个异步上下文中被等待,可能会导致死锁。 在 await 关键字后面使用 ConfigureAwait(false) 可以指示异步操作不必在原来的上下文中继续执行,而是可以在任何上下文中执行。这可以帮助避免某些特定的死锁情况。 避免阻塞await,不要在await后使用同步方法(如.Result或.Wait()),以免引发死锁。
# 与Task的区别
你可能会说:使用直接用Task.Run()不比async/await来的简单啊,看下面的例子
使用async/await的例子:
private async void loadButton_Click(object sender, EventArgs e)
{
// 显示加载中的提示
textBox.Text = "Loading...";
try
{
// 异步加载数据
string data = await LoadDataAsync();
// 加载成功后更新 TextBox
textBox.Text = data;
}
catch (Exception ex)
{
// 加载失败时显示错误信息
textBox.Text = "Error: " + ex.Message;
}
}
// 异步方法,用于从远程资源加载数据
private async Task<string> LoadDataAsync()
{
// 模拟一个耗时的操作,实际应用中应该是从网络加载数据
await Task.Delay(2000);
// 返回加载的数据
return "Loaded data from remote resource.";
}
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
在这个版本中,我们使用 async/await 关键字来定义异步方法 LoadDataAsync(),用于加载数据。在事件处理方法 loadButton_Click 中,我们直接调用 LoadDataAsync() 方法,并使用 await 关键字等待数据加载完成。加载成功后,我们直接更新 TextBox 控件的文本。
使用Task.Run的例子:
private void loadButton_Click(object sender, EventArgs e)
{
// 显示加载中的提示
textBox.Text = "Loading...";
// 使用 Task.Run() 启动异步操作
Task.Run(() =>
{
// 异步加载数据
string data = LoadData();
// 在 UI 线程上更新 TextBox 控件
textBox.Invoke((Action)(() => textBox.Text = data));
});
}
// 用于从远程资源加载数据的方法
private string LoadData()
{
// 模拟一个耗时的操作,实际应用中应该是从网络加载数据
Task.Delay(2000).Wait();
// 返回加载的数据
return "Loaded data from remote resource.";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在这个版本中,使用 Task.Run() 启动了异步操作,并在其中调用了 LoadData() 方法。在 LoadData() 方法中,我们模拟了从远程资源加载数据的耗时操作。然后,我们使用 Invoke 方法将加载的数据更新到 TextBox 控件中,确保这个操作在 UI 线程上执行。
通过比较两种方式,可以清楚地看到使用 async/await 的方法更简单明了。它不需要创建额外的线程,也不需要在异步操作完成后通过 Invoke 方法将结果更新到 UI 线程上,这样可以减少代码量并提高可读性。