在 C#中从同步代码调用异步代码

作者:微信公众号:【架构师老卢】
8-17 17:49
14

概述:如今,许多库代码都会使用 Task 异步编程模型 (TAP)。这些方法用 async 关键字标记,调用方必须使用 await 关键字来调用这些方法。但是,如果您有一个使用同步方法调用编写的旧应用程序,则在调用异步代码时应用 await 关键字可能不切实际。在调用异步方法时,使用 await 关键字需要在调用者方法上放置 async 关键字。当此调用方的调用方调用它时,它必须使用 await 关键字。这种级联效应可能需要您更改大量遗留代码以使其异步。尽管将同步代码更改为异步代码是利用异步库的理想方式,但实际上,这可能并不切实际。作为替代方案,许多人只是跳过 await 关键字并使用 .异步调用结

如今,许多库代码都会使用 Task 异步编程模型 (TAP)。这些方法用 async 关键字标记,调用方必须使用 await 关键字来调用这些方法。

但是,如果您有一个使用同步方法调用编写的旧应用程序,则在调用异步代码时应用 await 关键字可能不切实际。在调用异步方法时,使用 await 关键字需要在调用者方法上放置 async 关键字。当此调用方的调用方调用它时,它必须使用 await 关键字。这种级联效应可能需要您更改大量遗留代码以使其异步。

尽管将同步代码更改为异步代码是利用异步库的理想方式,但实际上,这可能并不切实际。作为替代方案,许多人只是跳过 await 关键字并使用 .异步调用结束时的结果,使其同步。以 Windows 窗体应用为例:

public partial class Form1 : Form  
{  
    public Form1()  
    {  
        InitializeComponent();  
    }  
  
    private void Form1_Load(object sender, EventArgs e)  
    {  
  
    }  
  
    private void button1_Click(object sender, EventArgs e)  
    {  
        MessageBox.Show(AsyncCall().Result);  
    }  
  
    private async Task<string> AsyncCall()  
    {  
        await Task.Delay(1000);  
        return await Task.FromResult("test");  
    }  
}

上述代码会导致死锁。我们稍后会解释原因。现在我们来解释一些关于多线程/异步编程的重要概念。

多线程编程的挑战

如果在代码中使用多线程,则在读取/写入变量值时必须非常小心。多个线程可以同时访问同一变量,从而创建争用条件。这种竞争条件很难调试。

因此,如果我们可以在线程之间设置一个明确的边界,那么每个线程只能访问它拥有的变量。如果一个线程需要更新另一个线程中的变量,它将向另一个线程发送一条消息以执行此操作。

SynchronizationContext

SynchronizationContext 类提供了一种方法,让一个线程将委托发布到另一个线程。然后,该委托将由拥有 The SynchronizationContext 的目标线程执行。

SynchronizationContext 类是一个基类。它提供了用于传播同步上下文的基本功能。它不提供任何同步。但是,同步模型的提供者可以扩展此类,并为这些方法提供自己的实现。例如,Windows 窗体中的 WindowsFormsSynchronizationContext 和 WPF 中的 DispatcherSynchronizationContext

SynchronizationContext 中的重要属性和方法

  • Current 属性
public static System.Threading.SynchronizationContext? Current { get; }

Current 属性获取线程的当前同步上下文。

  • Post() 方法
public virtual void Post (System.Threading.SendOrPostCallback d, object? state);

Post() 异步向 SynchronizationContext 发送消息。

  • Send() 方法
public virtual void Send (System.Threading.SendOrPostCallback d, object? state);

Send() 同步向 SynchronizationContext 发送消息。

  • SetSynchronizationContext() 方法
public static void SetSynchronizationContext (System.Threading.SynchronizationContext? syncContext);

设置当前同步上下文。

每个线程都有一个 SynchronizationContext 对象。但是,一个 SynchronizationContext 对象可以由多个线程共享。

您可以将 SynchronizationContext 视为消息队列。您可以将代理排队到该代理。该委托将由拥有此 SynchronizationContext 的“_主”_线程执行。我所说的“_主”_线程是指具有处理消息队列和执行委托的逻辑的线程。通常,您可以让多个线程共享相同的 SynchronizationContext,但只有一个线程具有处理消息的逻辑,其他线程只是将该线程要执行的委托排队。

Windows 窗体 UI 线程

Windwos Forms UI线程是创建所有表单,控件并调用Application.Run()的线程。与控件相关的 UI 的所有更新都必须在此 UI 线程上运行。

此线程具有 WindowsFormsSynchronizationContext。

等待

当编译器嵌入者等待关键字时,它确实会生成类似

  1. 使用 SynchronizationContext.Current 属性获取当前线程的 SynchronizationContext。
  2. 启动新任务/线程。当前线程(主线程)仍然具有控制权,并且不会被异步调用阻塞。
  3. 在新线程中,将新线程的 SynchronizationContext 设置为步骤 1 中的那个。
  4. 在新线程中,执行异步调用并等待结果。
  5. 当异步调用完成并且结果准备就绪时,运行时会将委托发布到它在步骤 1 中获取的 SynchronizationContext。委托是 await 语句之后的其余代码。
  6. 主线程将从消息队列中选取委托并执行它。

使用 ConfigureAwait(false) 等待

默认情况下,如果在 await 语句中不使用 ConfigureAwait() 方法,则它与 ConfigureAwait(true) 相同。该参数表示在异步调用完成后,运行时必须在 await 使用捕获的原始上下文后继续执行其余代码。true

该参数告诉运行时无需使用原始捕获的上下文执行其余代码。相反,运行时可以使用调用异步方法的同一线程直接调用其余代码。false

使用有两个好处:false

  1. 提高性能。对回调进行排队而不是仅仅调用它,是有代价的。
  2. 避免死锁。如果调用方使用 同步调用 async 方法。结果,可能会发生死锁。

死锁场景解释

回到我们之前在本文中介绍的示例。我们有一个 async 方法 。在它里面,我们有一个 await 语句,没有 .ConfigureAwait(),这意味着在 await 调用之后,运行时必须使用原始上下文继续其余代码。在本例中,oringal 上下文是 UI 线程上下文 (WindowsFormsSynchronizationContext)。AsyncCall()

当 AsyncCall() 时。结果被调用,它将阻塞当前线程、UI 线程。当等待时 Task.Delay(1000); 完成后,它将向 UI 线程上下文 (WindowsFormsSynchronizationContext) 发布一条消息。但是,由于 UI 线程被 阻止。结果调用时,没有线程来处理此委托消息,因此死锁。

避免死锁的解决方案

为了避免死锁,我们有两种 3 种可能的解决方案:

  1. 在所有应用程序中使用 await。不要同步调用异步方法。从技术角度来看,这是最好的解决方案。但是,如果您正在处理大型遗留应用程序,则这可能不切实际。
  2. 添加 .ConfigureAwait(false) 添加到通用库代码中的所有异步方法。
  3. 同步调用任何异步方法时,请使用 Task.Run(异步调用)将其包装。结果。例如,在上面的示例中,我们可以更改 AsyncCall()。_Task.Run(() => AsyncCall()) 的结果。结果_以避免死锁。

避免死锁的解决方案 3 解释

以下是更改后的代码:

public partial class Form1 : Form  
{  
    public Form1()  
    {  
        InitializeComponent();  
    }  
  
    private void Form1_Load(object sender, EventArgs e)  
    {  
  
    }  
  
    private void button1_Click(object sender, EventArgs e)  
    {  
        MessageBox.Show(Task.Run(() => AsyncCall()).Result);  
    }  
  
    private async Task<string> AsyncCall()  
    {  
        await Task.Delay(1000);  
        return await Task.FromResult("test");  
    }  
}

以下是我们使用 Task.Run() 执行异步调用,然后同步获取结果时发生的步骤。

  1. 当达到 Task.Run() 时,运行时将从线程池创建一个线程。运行时返回 Task 对象。
  2. 这。Task.Run() 末尾的结果将阻塞当前 UI 线程。
  3. 在新创建的线程中,将调用 AysncCall() 方法。当它到达 await Task.Delay(1000) 时,运行时将使用 SynchronizationContext.Current 属性获取当前线程的 SynchronizationContext。由于当前线程是线程池线程,因此 SynchronizationContext 实际上是 null。
  4. await 语句将创建一个新线程。异步调用在新线程内执行。
  5. 当异步调用完成且结果准备就绪时,运行时将使用步骤 1 中创建的线程执行其余代码,并将结果返回到主 UI 线程。没有向 UI 线程的 SynchronizationContext 发布委托。
  6. UI 线程获取异步调用结果并继续。
阅读排行