65.9K
CodeProject 正在变化。 阅读更多。
Home

理解同步上下文;Task.ConfigureAwait 的实际应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2021年8月28日

CPOL

10分钟阅读

viewsIcon

15662

SynchronizationContext 类及其如何影响代码行为,以及 Task.ConfigureAwait() 的探讨

引言

同步上下文是线程运行的环境。它是一组特性,定义了线程如何响应消息。可以将其视为定义线程边界的范围。在不通过目标线程的同步上下文进行通信的情况下,任何线程都无法访问其他线程的数据,也无法将任何工作单元传递给其他线程。值得一提的是,不同的线程可能共享同一个同步上下文,并且线程拥有同步上下文是可选的。

一点历史

如果你有 Windows 开发背景,你肯定知道在 WinForms 中,例如,你不能从任何其他线程访问 UI 控件。你必须通过 `Control.BeginInvoke()` 函数,或者更精确地说,使用 `ISynchronizeInvoke` 模式,将你的任务卸载,或者换句话说,将你的代码(工作单元)委托给 UI 线程。`ISynchronizeInvoke` 允许你将一个工作单元排队到 UI 线程进行处理。如果你不遵循此模式,你的代码可能会因**跨线程访问**错误而崩溃。

然后,引入了同步上下文。实际上,对于 WinForms 而言,它内部使用了 `ISynchronizeInvoke` 模式。同步上下文的引入使得对线程环境和边界有了共同的理解。你不再需要单独考虑每个框架,你必须理解同步上下文是什么,以及如何将工作委托给其他线程进行处理。

实现

尽管同步上下文在各种 .NET 平台中的实现方式不同,但其思想是相同的,并且所有实现都派生自默认的 .NET 同步上下文类 `SynchronizationContext` (`mscorlib.dll: System`),该类引入了一个静态属性 `Current`,它返回当前线程的 `SynchronizationContext`,以及两个主要方法 `Send()` 和 `Post()`。

简而言之,`Send()` 和 `Post()` 的区别在于 `Send()` 将同步运行此工作单元。换句话说,当源线程将一些工作委托给另一个线程时,它将被阻塞,直到目标线程完成。另一方面,如果使用 `Post()`,则调用线程不会被阻塞。请参阅下图

当我们查看默认 `SynchronizationContext` 的内部实现时,我们可以看到以下内容

public virtual void Send(SendOrPostCallback d, object state) => d(state);

public virtual void Post(SendOrPostCallback d, object state) => 
    ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);

`Send()` 只是执行 `delegate`,而 `Post()` 使用 `ThreadPool` 异步执行 `delegate`。

请注意,尽管 `Send()` 和 `Post()` 默认行为不同,但当它们由平台实现时可能不会。现在让我们快速了解一下各种平台如何实现同步上下文。

WinForms

在 Windows Forms 应用程序中,同步上下文通过 `WindowsFormsSynchronizationContext` (`System.Windows.Forms.dll: System.Windows.Forms`) 实现,它本身派生自默认的 `SynchronizationContext` (`mscorlib.dll: System`)。此实现分别调用 `Control.Invoke()` 和 `Control.BeginInvoke()` 来实现 `Send()` 和 `Post()`。它们确保工作单元被发送到 UI 线程。

`WindowsFormsSynchronizationContext` 何时应用?当你第一次实例化窗体时,它会被应用(即安装)。请注意,这些断言将会通过

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);

  // SynchronizationContext is still null here
  Debug.Assert(SynchronizationContext.Current == null);

  var frm = new TasksForm();

  // Now SynchronizationContext is installed
  Debug.Assert(SynchronizationContext.Current != null && 
    SynchronizationContext.Current is WindowsFormsSynchronizationContext);

  Application.Run(frm);
}

WPF 和 Silverlight

在 WPF 和 Silverlight 中,同步上下文通过 `DispatcherSynchronizationContext` (`WindowsBase.dll: System.Windows.Threading`) 实现,它的作用与 WinForms 的对应项相同,它将委托的工作单元传递给 UI 线程执行,并在内部使用 `Dispatcher.Invoke()` 和 `Dispatcher.BeginInvoke()` 进行委托。

经典 ASP.NET

在经典的 ASP.NET 中,同步上下文通过 `AspNetSynchronizationContext` (`System.Web.dll: System.Web`) 实现,然而,它的实现方式与其他平台中的对应项不同。由于 ASP.NET 中没有 UI 线程的概念,并且每个请求都需要一个单独的线程进行处理,因此 `AspNetSynchronizationContext` 只是维护一个未完成操作的队列,当所有操作完成后,请求就可以标记为已完成。`Post()` 仍然将委托的工作传递给 `ThreadPool`。

`AspNetSynchronizationContext` 的唯一用途是这个吗?不是。`AspNetSynchronizationContext` 最重要的任务是确保请求线程可以访问 `HttpContext.Current` 和其他相关的身份和文化数据。你知道这一点。有时你在运行线程之前缓存 `HttpContext.Current` 的引用,这就是 `AspNetSynchronizationContext` 派上用场的时候。

ASP.NET Core

ASP.NET Core 中没有 `AspNetSynchronizationContext` 的等价物,它已被移除。为什么?它是应用于新平台的各种性能改进的一部分。它使应用程序摆脱了进入和离开同步上下文时产生的开销(我们稍后会讨论)。请在此处阅读更多关于 ASP.NET Core 的 SynchronizationContext 信息

同步上下文的实际应用

现在,让我们看看同步上下文的实际应用。在此示例中,我们将看到如何将一个工作单元从工作线程传递到 UI 线程。启动一个新的 WinForms 项目并更新设计器代码以匹配以下内容

private void InitializeComponent()
{
  this.ResultsListBox = new System.Windows.Forms.ListBox();
  this.RegularThreadsButton = new System.Windows.Forms.Button();
  this.UIThreadTest = new System.Windows.Forms.Button();
  this.SuspendLayout();
  // 
  // ResultsListBox
  // 
  this.ResultsListBox.FormattingEnabled = true;
  this.ResultsListBox.Location = new System.Drawing.Point(12, 12);
  this.ResultsListBox.Name = "ResultsListBox";
  this.ResultsListBox.Size = new System.Drawing.Size(516, 212);
  this.ResultsListBox.TabIndex = 1;
  // 
  // RegularThreadsButton
  // 
  this.RegularThreadsButton.Location = new System.Drawing.Point(12, 232);
  this.RegularThreadsButton.Name = "RegularThreadsButton";
  this.RegularThreadsButton.Size = new System.Drawing.Size(516, 23);
  this.RegularThreadsButton.TabIndex = 2;
  this.RegularThreadsButton.Text = "Regular Thread Test";
  this.RegularThreadsButton.UseVisualStyleBackColor = true;
  this.RegularThreadsButton.Click += new System.EventHandler(this.RegularThreadsButton_Click);
  // 
  // UIThreadTest
  // 
  this.UIThreadTest.Location = new System.Drawing.Point(12, 261);
  this.UIThreadTest.Name = "UIThreadTest";
  this.UIThreadTest.Size = new System.Drawing.Size(516, 23);
  this.UIThreadTest.TabIndex = 3;
  this.UIThreadTest.Text = "UI-Context Thread Test";
  this.UIThreadTest.UseVisualStyleBackColor = true;
  this.UIThreadTest.Click += new System.EventHandler(this.UIThreadTest_Click);
  // 
  // ThreadsForm
  // 
  this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
  this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
  this.ClientSize = new System.Drawing.Size(540, 294);
  this.Controls.Add(this.UIThreadTest);
  this.Controls.Add(this.RegularThreadsButton);
  this.Controls.Add(this.ResultsListBox);
  this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
  this.MaximizeBox = false;
  this.Name = "ThreadsForm";
  this.Text = "SynchronizationContext Sample";
  this.ResumeLayout(false);

}

private System.Windows.Forms.ListBox ResultsListBox;
private System.Windows.Forms.Button RegularThreadsButton;
private System.Windows.Forms.Button UIThreadTest;

现在转到窗体代码,并添加以下内容

private void RegularThreadsButton_Click(object sender, EventArgs e)
{
  RunThreads(null);
}
private void UIThreadTest_Click(object sender, EventArgs e)
{
  // SynchronizationContext.Current will return 
  // a reference to WindowsFormsSynchronizationContext
  RunThreads(SynchronizationContext.Current);
}

private void RunThreads(SynchronizationContext context)
{
  this.ResultsListBox.Items.Clear();
  this.ResultsListBox.Items.Add($"UI Thread {Thread.CurrentThread.ManagedThreadId}");  
  this.ResultsListBox.Items.Clear();


  int maxThreads = 3;
  for (int i = 0; i < maxThreads; i++)
  {
    Thread t = new Thread(UpdateListBox);
    t.IsBackground = true;
    t.Start(context); // passing context to thread proc
  }
}

private void UpdateListBox(object state)
{
  // fetching passed SynchrnozationContext
  SynchronizationContext syncContext = state as SynchronizationContext;

  // get thread ID
  var threadId = Thread.CurrentThread.ManagedThreadId;

  if (null == syncContext) // no SynchronizationContext provided
    this.ResultsListBox.Items.Add($"Hello from thread {threadId}, 
    currently executing thread is {Thread.CurrentThread.ManagedThreadId}");
  else
    syncContext.Send((obj) => this.ResultsListBox.Items.Add
    ($"Hello from thread {threadId}, 
    currently executing thread is {Thread.CurrentThread.ManagedThreadId}"), null);
}

上述代码简单地启动了三个线程,它们只是将记录添加到列表框中,以说明当前调用和执行的线程。现在**以调试模式**运行代码,然后点击“**常规线程测试**”按钮。执行将暂停,你将收到以下异常

这里的关键是。默认情况下,我们无法从任何其他线程访问其他线程的数据(在本例中为控件)。当你这样做时,你会收到一个包装在 `InvalidOperationException` 异常中的**跨线程操作错误**。

这里有一个小提示:当你脱离调试模式工作时,WinForms 将忽略此异常。要始终启用此异常,请在 `Application.Run()` 之前将以下行添加到 `Main()` 方法中。

Control.CheckForIllegalCrossThreadCalls = true;

现在,再次运行应用程序并点击“**UI-上下文线程测试**”按钮。

由于代码通过 `SynchronizationContext.Send()` 将执行传递给主 UI 线程,我们现在在结果中可以看到调用线程是不同的,但是,工作已传递给主线程(线程 1),该线程已成功处理了代码。

任务和同步上下文

同步上下文是 `async`/`await` 模式的核心部分。当你 `await` 一个任务时,你会暂停当前 `async` 方法的执行,直到给定任务的执行完成。

让我们深入探讨上述图示的更多细节。`await` 不仅仅是等待工作线程完成!大致上,`await` 在执行异步任务之前捕获当前的同步上下文(**离开**当前同步上下文)。异步任务返回后,它会再次引用原始同步上下文(**重新进入**同步上下文),然后方法的其余部分继续执行。

让我们看看实际效果。启动一个新项目或向现有项目添加一个新窗体。转到新窗体的设计器代码并更新以匹配以下内容

private void InitializeComponent()
{
  this.ResultsListBox = new System.Windows.Forms.ListBox();
  this.NoContextButton = new System.Windows.Forms.Button();
  this.UIContextButton = new System.Windows.Forms.Button();
  this.SuspendLayout();
  // 
  // ResultsListBox
  // 
  this.ResultsListBox.FormattingEnabled = true;
  this.ResultsListBox.Location = new System.Drawing.Point(13, 13);
  this.ResultsListBox.Name = "ResultsListBox";
  this.ResultsListBox.Size = new System.Drawing.Size(429, 264);
  this.ResultsListBox.TabIndex = 0;
  // 
  // NoContextButton
  // 
  this.NoContextButton.Location = new System.Drawing.Point(13, 284);
  this.NoContextButton.Name = "NoContextButton";
  this.NoContextButton.Size = new System.Drawing.Size(429, 23);
  this.NoContextButton.TabIndex = 1;
  this.NoContextButton.Text = "Task without Synchronization Context";
  this.NoContextButton.UseVisualStyleBackColor = true;
  this.NoContextButton.Click += new System.EventHandler(this.NoContextButton_Click);
  // 
  // UIContextButton
  // 
  this.UIContextButton.Location = new System.Drawing.Point(13, 313);
  this.UIContextButton.Name = "UIContextButton";
  this.UIContextButton.Size = new System.Drawing.Size(429, 23);
  this.UIContextButton.TabIndex = 2;
  this.UIContextButton.Text = "Task with UI Synchronization Context";
  this.UIContextButton.UseVisualStyleBackColor = true;
  this.UIContextButton.Click += new System.EventHandler(this.UIContextButton_Click);
  // 
  // TasksForm
  // 
  this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
  this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
  this.ClientSize = new System.Drawing.Size(454, 345);
  this.Controls.Add(this.UIContextButton);
  this.Controls.Add(this.NoContextButton);
  this.Controls.Add(this.ResultsListBox);
  this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
  this.MaximizeBox = false;
  this.Name = "TasksForm";
  this.Text = "TasksForm";
  this.ResumeLayout(false);
}

private System.Windows.Forms.ListBox ResultsListBox;
private System.Windows.Forms.Button NoContextButton;
private System.Windows.Forms.Button UIContextButton;

现在,转到窗体代码并添加以下内容

private void NoContextButton_Click(object sender, EventArgs e)
{
  RunTask(null);
}

private void UIContextButton_Click(object sender, EventArgs e)
{
  RunTask(SynchronizationContext.Current);
}

private void RunTask(SynchronizationContext context)
{
  this.ResultsListBox.Items.Clear();
  this.ResultsListBox.Items.Add($"UI Thread {Thread.CurrentThread.ManagedThreadId}");

  Task.Run(async () =>
  {
    if (null != context)
      SynchronizationContext.SetSynchronizationContext(context);

    LogMessage($"Task started");

    if (null == SynchronizationContext.Current)
      LogMessage($"Task synchronization context is null");
    else
      LogMessage($"Task synchronization context is 
                {SynchronizationContext.Current.GetType().Name}");

    await Task.Delay(1000);

    LogMessage($"Task thread is {Thread.CurrentThread.ManagedThreadId}");

    LogMessage($"Control.InvokeRequired = {this.ResultsListBox.InvokeRequired}");

    LogMessage($"Trying to manipulate UI...");
    try
    {
      this.ResultsListBox.Items.Add("Successfully accessed UI directly!");
    }
    catch (InvalidOperationException)
    {
      LogMessage($"Failed!");
    }

    LogMessage($"Task finished");
  });
}

private void LogMessage(string msg)
{
  this.ResultsListBox.Invoke((Action)(() =>
  {
    this.ResultsListBox.Items.Add(msg);
  }));
}

上述代码简单地提供了两个选项,一个不设置任务的同步上下文,使其为 `null`,另一个将其设置为 UI 线程的同步上下文。代码等待一个任务并测试当前线程的 UI 可访问性。当我们运行应用程序并点击无上下文按钮时,我们得到以下结果

`await` 之后的代码在不同的线程中运行,并且无法直接访问控件。现在,点击 UI-上下文按钮并查看结果

第二个选项只是使用 `SynchronizationContext.SetSynchronizationContext()` 调用将同步上下文设置为 UI 线程同步上下文。这影响了我们的行为,当我们调用 `await` 时,它捕获了当前的同步上下文(即 `WinFormsSynchronizationContext`),然后**离开**当前上下文到给定的任务并等待其完成。任务完成后,它再次**重新进入**当前上下文,并且你已经能够使用 UI 线程访问 UI 控件,而无需任何委托或回调。

这里有一个小提示,你可能会问自己为什么我们必须使用 `SetSynchronizationContext()`?! `await` 不是应该自动捕获同步上下文吗?是的,它是。但是由于我们正在新任务的上下文中运行(我们使用了 `Task.Run()`),它没有同步上下文。默认情况下,工作任务和线程没有同步上下文(你可以通过检查 `SynchronizationContext.Current` 来验证)。这就是为什么我们必须在调用 `Task.Run()` 之前首先引用 UI 上下文,然后我们必须使用 `SetSynchronizationContext()` 来设置它。在承诺风格的任务和 `Task.Run()` 之外,你可以使用 `ConfigureAwait()` 选项,稍后会解释。

ConfigureAwait 的实际应用

同步上下文的核心概念之一是**上下文切换**。当你 await 一个任务时,就会发生上下文切换。在等待任务之前,你会**捕获**当前上下文,**将其留给**任务上下文,然后在任务完成时**恢复**(**重新进入**)它。这个过程开销很大,在许多情况下,你不需要它!例如,如果在任务之后你不处理 UI 控件,为什么还要再次切换回原始上下文?为什么不节省时间并避免这一轮呢?

经验法则表明,如果你正在开发一个库,或者你不需要访问 UI 控件,或者你可以引用同步数据(如 `HttpContext.Current`)以备将来使用,请节省时间和精力,禁用上下文切换。

这时 `Task.ConfigureAwait()` 就派上用场了。它有一个参数 `continueOnCapturedContext`,如果设置为 `true`(如果未使用 `ConfigureAwait()` 则为默认行为),则启用上下文恢复;如果设置为 `false`,则禁用上下文恢复。

让我们看看实际效果。

WinForms 中的 ConfigureAwait

启动一个新的 WinForms 项目或向现有项目添加一个新窗体。切换到窗体设计器代码并更新以匹配以下内容

private void InitializeComponent()
{
      this.ResultsListBox = new System.Windows.Forms.ListBox();
      this.ConfigureTrueButton = new System.Windows.Forms.Button();
      this.ConfigureFalseButton = new System.Windows.Forms.Button();
      this.SuspendLayout();
      // 
      // ResultsListBox
      // 
      this.ResultsListBox.FormattingEnabled = true;
      this.ResultsListBox.Location = new System.Drawing.Point(12, 12);
      this.ResultsListBox.Name = "ResultsListBox";
      this.ResultsListBox.Size = new System.Drawing.Size(517, 342);
      this.ResultsListBox.TabIndex = 0;
      // 
      // ConfigureTrueButton
      // 
      this.ConfigureTrueButton.Location = new System.Drawing.Point(12, 357);
      this.ConfigureTrueButton.Name = "ConfigureTrueButton";
      this.ConfigureTrueButton.Size = new System.Drawing.Size(516, 23);
      this.ConfigureTrueButton.TabIndex = 1;
      this.ConfigureTrueButton.Text = "Task.ConfigureAwait(true) Test";
      this.ConfigureTrueButton.UseVisualStyleBackColor = true;
      this.ConfigureTrueButton.Click += 
           new System.EventHandler(this.ConfigureTrueButton_Click);
      // 
      // ConfigureFalseButton
      // 
      this.ConfigureFalseButton.Location = new System.Drawing.Point(12, 386);
      this.ConfigureFalseButton.Name = "ConfigureFalseButton";
      this.ConfigureFalseButton.Size = new System.Drawing.Size(516, 23);
      this.ConfigureFalseButton.TabIndex = 2;
      this.ConfigureFalseButton.Text = "Task.ConfigureAwait(false) Test";
      this.ConfigureFalseButton.UseVisualStyleBackColor = true;
      this.ConfigureFalseButton.Click += 
           new System.EventHandler(this.ConfigureFalseButton_Click);
      // 
      // ConfigureAwaitForm
      // 
      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      this.ClientSize = new System.Drawing.Size(541, 421);
      this.Controls.Add(this.ConfigureFalseButton);
      this.Controls.Add(this.ConfigureTrueButton);
      this.Controls.Add(this.ResultsListBox);
      this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
      this.MaximizeBox = false;
      this.Name = "ConfigureAwaitForm";
      this.Text = "Task.ConfigureAwait Sample";
      this.ResumeLayout(false);
}

private System.Windows.Forms.ListBox ResultsListBox;
private System.Windows.Forms.Button ConfigureTrueButton;
private System.Windows.Forms.Button ConfigureFalseButton;

现在切换到窗体代码并添加以下内容

private void ConfigureTrueButton_Click(object sender, EventArgs e)
{
  AsyncTest(true);
}

private void ConfigureFalseButton_Click(object sender, EventArgs e)
{
  AsyncTest(false);
}

private async void AsyncTest(bool configureAwait)
{
  this.ResultsListBox.Items.Clear();
  try
  {
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("ar-EG");

    this.ResultsListBox.Items.Add("Async test started");
    this.ResultsListBox.Items.Add(string.Format("configureAwait = {0}", configureAwait));

    this.ResultsListBox.Items.Add(string.Format
    ("Current thread ID = {0}", Thread.CurrentThread.ManagedThreadId));
    this.ResultsListBox.Items.Add(string.Format
    ("Current culture = {0}", Thread.CurrentThread.CurrentCulture));

    this.ResultsListBox.Items.Add("Awaiting a task...");
    await Task.Delay(500).ConfigureAwait(configureAwait);

    this.ResultsListBox.Items.Add("Task completed");
    this.ResultsListBox.Items.Add(string.Format
    ("Current thread ID: {0}", Thread.CurrentThread.ManagedThreadId));
    this.ResultsListBox.Items.Add(string.Format
    ("Current culture: {0}", Thread.CurrentThread.CurrentCulture));
  }
  catch (InvalidOperationException ex)
  {
    var threadId = Thread.CurrentThread.ManagedThreadId;

    this.ResultsListBox.BeginInvoke((Action)(() =>
    {
      this.ResultsListBox.Items.Add($"{ex.GetType().Name} caught from thread {threadId}");
    }));
  }
}

该代码只是等待一个任务,并根据点击的按钮切换 `ConfigureAwait()`。它还在切换之前更改当前线程的文化信息。运行窗体并点击“**ConfigureAwait(true)**”按钮。

行为如预期。我们恢复了原始同步上下文,保留了线程环境数据(如文化),并且能够轻松直接访问 UI 控件。

现在点击“**ConfigureAwait(false)**”按钮并查看结果

当将 `ConfigureAwait.continueOnCapturedContext` 设置为 `false` 时,我们无法返回到原始上下文,并且由于跨线程访问而收到了 `InvalidOperationException` 错误。

ASP.NET MVC 中的 ConfigureAwait

启动一个新的 MVC 项目,并更新 *index.cshtml* 文件以匹配以下内容

@model IEnumerable<String>

@if (null != Model && Model.Any())
{
  <ul>
    @foreach (var val in Model)
    {
      <li>@val</li>
    }
  </ul>
}

现在,转到 `Home` 控制器并添加以下代码

private List<string> results = new List<string>();

public async Task<ActionResult> Index(bool configureAwait = false)
{
  await AsyncTest(configureAwait);
  return View(results);
} 

private async Task AsyncTest(bool configureAwait)
{
  results.Add($"Async test started, ConfigureAwait = {configureAwait}");

  if (null == System.Web.HttpContext.Current)
    results.Add($"HttpContext.Current is null");
  else
    results.Add($"HttpContext.Current is NOT null");

  results.Add($"Current thread ID = {Thread.CurrentThread.ManagedThreadId}");

  results.Add("Awaiting task...");

  await Task.Delay(1000).ConfigureAwait(configureAwait);

  results.Add("Task completed");

  results.Add($"Current thread ID = {Thread.CurrentThread.ManagedThreadId}");

  if (null == System.Web.HttpContext.Current)
    results.Add($"HttpContext.Current is null");
  else
    results.Add($"HttpContext.Current is NOT null");
}

运行应用程序并检查两种情况之间的区别

你现在可以看到,当将 `ConfigureAwait.continueOnCapturedContext` 设置为 `false` 时,原始同步上下文不会恢复,并且我们失去了对 `HttpContext.Current` 的访问。另一方面,当设置为 `true` 时,我们恢复了原始同步上下文,并获得了对 `HttpContext.Current` 的访问。请注意,这里切换到原始线程并不相关,因为与桌面应用程序不同,ASP.NET 中没有 UI 线程。

最后说明

我们可以将以上所有内容总结为两点

  • 为了更好的性能,当在库中或在 `await` 之后不需要访问 UI 元素时,请使用 `ConfigureAwait(false)`。
  • 当需要在线程场景中重新捕获原始上下文时,请使用 `SynchronizationContext.Set()`。

最后,我希望我能够简化事情并展示同步上下文的各个方面。请随时与我分享你的想法和反馈。

代码可在 GitHub 上获取:

历史

  • 2021年8月28日:初始版本
© . All rights reserved.