C#多线程之异步线程🔥

1/17/2024 多线程

在 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++;
    }
}
1
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);
}
1
2
3
4

如果异步方法返回一个值,则返回类型应为

Task<TResult>
1

其中 TResult 是返回值的类型。例如:

public async Task<int> GetResultAsync() 
{ 
    await DoSomethingAsync();
    return 1;
}
1
2
3
4
5

最后一种是返回void,多用于事件触发器等特殊情况,例如,在 Windows Forms 或 WPF 应用程序中,处理按钮点击事件的方法通常是 void,因为事件处理器不能返回 Task。

private async void Button_Click(object sender, EventArgs e)
{
    // 异步操作
    await SomeAsyncOperation();
}
1
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");
    }
}
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
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");
    }
}
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
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.";
}
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

在这个版本中,我们使用 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.";
}
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

在这个版本中,使用 Task.Run() 启动了异步操作,并在其中调用了 LoadData() 方法。在 LoadData() 方法中,我们模拟了从远程资源加载数据的耗时操作。然后,我们使用 Invoke 方法将加载的数据更新到 TextBox 控件中,确保这个操作在 UI 线程上执行。

通过比较两种方式,可以清楚地看到使用 async/await 的方法更简单明了。它不需要创建额外的线程,也不需要在异步操作完成后通过 Invoke 方法将结果更新到 UI 线程上,这样可以减少代码量并提高可读性。