MST

星途 面试题库

面试题:C#异步编程中的死锁问题及解决

在C#异步编程中,可能会遇到死锁情况。请阐述可能导致死锁的场景,例如在使用`async`/`await`与同步上下文(SynchronizationContext)结合时,并说明如何避免这种死锁。
20.4万 热度难度
编程语言C#

知识考点

AI 面试

面试题答案

一键面试

可能导致死锁的场景

  1. async/awaitSynchronizationContext 结合时
    • 在WinForms或WPF应用程序中,SynchronizationContext会捕获当前线程的上下文。假设在UI线程上启动一个异步操作,该异步操作在await处暂停,然后当await后的代码恢复执行时,它会尝试在原始的同步上下文(即UI线程)上执行。如果此时UI线程被阻塞,就会导致死锁。例如,当异步方法中await一个任务,而这个任务又需要获取UI线程上的资源(比如更新UI控件),同时UI线程正在等待这个异步任务完成,就会形成死锁。
    • 代码示例:
private async void Button_Click(object sender, EventArgs e)
{
    // 在UI线程上开始
    var task = Task.Run(() =>
    {
        // 模拟一些工作
        Thread.Sleep(2000);
        // 这里尝试获取UI线程上的资源,比如更新UI
        // 假设这里有一个需要在UI线程执行的方法UpdateUI()
        // 但此时UI线程可能正在等待下面的await完成
        // 会导致死锁
    });
    await task;
}
  1. 混合使用同步和异步代码
    • 当在异步方法中调用同步阻塞方法,而同步阻塞方法又等待异步操作完成时,可能会导致死锁。例如,在async方法中使用Task.Wait()Task.Result来等待一个任务完成,而这个任务可能需要回到调用Wait()Result的线程上下文来执行后续代码,就会形成死锁。
    • 代码示例:
public async Task DoAsyncWork()
{
    var task = Task.Run(() =>
    {
        // 模拟工作
        Thread.Sleep(2000);
        return 42;
    });
    // 错误的做法,会导致死锁
    int result = task.Result; 
    // 或者 task.Wait();
}

避免死锁的方法

  1. 避免在异步代码中使用同步阻塞操作
    • 不要在async方法中使用Task.Wait()Task.Result,而是始终使用await来等待任务完成。
    • 例如,将上述代码修改为:
public async Task DoAsyncWork()
{
    var task = Task.Run(() =>
    {
        // 模拟工作
        Thread.Sleep(2000);
        return 42;
    });
    int result = await task; 
}
  1. 正确处理同步上下文
    • 在WinForms或WPF应用程序中,可以使用ConfigureAwait(false)来避免在await后恢复到原始的同步上下文。这样可以允许await后的代码在ThreadPool线程上执行,而不是一定回到UI线程。例如:
private async void Button_Click(object sender, EventArgs e)
{
    var task = Task.Run(() =>
    {
        // 模拟一些工作
        Thread.Sleep(2000);
        return 42;
    });
    int result = await task.ConfigureAwait(false);
    // 如果需要更新UI,使用BeginInvoke或Dispatcher.Invoke回到UI线程
    // 例如在WinForms中:
    this.BeginInvoke((Action)(() =>
    {
        // 更新UI代码
        label1.Text = result.ToString();
    }));
}
  • 这样可以防止因为UI线程等待异步任务,而异步任务又等待UI线程导致的死锁。