如今,许多库代码都会使用 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 类提供了一种方法,让一个线程将委托发布到另一个线程。然后,该委托将由拥有 The SynchronizationContext 的目标线程执行。
SynchronizationContext 类是一个基类。它提供了用于传播同步上下文的基本功能。它不提供任何同步。但是,同步模型的提供者可以扩展此类,并为这些方法提供自己的实现。例如,Windows 窗体中的 WindowsFormsSynchronizationContext 和 WPF 中的 DispatcherSynchronizationContext。
public static System.Threading.SynchronizationContext? Current { get; }
Current 属性获取线程的当前同步上下文。
public virtual void Post (System.Threading.SendOrPostCallback d, object? state);
Post() 异步向 SynchronizationContext 发送消息。
public virtual void Send (System.Threading.SendOrPostCallback d, object? state);
Send() 同步向 SynchronizationContext 发送消息。
public static void SetSynchronizationContext (System.Threading.SynchronizationContext? syncContext);
设置当前同步上下文。
每个线程都有一个 SynchronizationContext 对象。但是,一个 SynchronizationContext 对象可以由多个线程共享。
您可以将 SynchronizationContext 视为消息队列。您可以将代理排队到该代理。该委托将由拥有此 SynchronizationContext 的“_主”_线程执行。我所说的“_主”_线程是指具有处理消息队列和执行委托的逻辑的线程。通常,您可以让多个线程共享相同的 SynchronizationContext,但只有一个线程具有处理消息的逻辑,其他线程只是将该线程要执行的委托排队。
Windwos Forms UI线程是创建所有表单,控件并调用Application.Run()的线程。与控件相关的 UI 的所有更新都必须在此 UI 线程上运行。
此线程具有 WindowsFormsSynchronizationContext。
当编译器嵌入者等待关键字时,它确实会生成类似
默认情况下,如果在 await 语句中不使用 ConfigureAwait() 方法,则它与 ConfigureAwait(true) 相同。该参数表示在异步调用完成后,运行时必须在 await 使用捕获的原始上下文后继续执行其余代码。true
该参数告诉运行时无需使用原始捕获的上下文执行其余代码。相反,运行时可以使用调用异步方法的同一线程直接调用其余代码。false
使用有两个好处:false
回到我们之前在本文中介绍的示例。我们有一个 async 方法 。在它里面,我们有一个 await 语句,没有 .ConfigureAwait(),这意味着在 await 调用之后,运行时必须使用原始上下文继续其余代码。在本例中,oringal 上下文是 UI 线程上下文 (WindowsFormsSynchronizationContext)。AsyncCall()
当 AsyncCall() 时。结果被调用,它将阻塞当前线程、UI 线程。当等待时 Task.Delay(1000); 完成后,它将向 UI 线程上下文 (WindowsFormsSynchronizationContext) 发布一条消息。但是,由于 UI 线程被 阻止。结果调用时,没有线程来处理此委托消息,因此死锁。
为了避免死锁,我们有两种 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() 执行异步调用,然后同步获取结果时发生的步骤。