如果我们想完全了解异步在 .Net 中的工作原理,SynchronizationContext 是值得更好理解的主题之一。确实,大多数这些问题都是在幕后处理的。但是我们可以通过了解当我们将任务卸载到工作线程或将线程释放回调用者时究竟发生了什么而受益。所以在这篇文章中,我将详细介绍SynchronizationContext
它是什么以及它的作用。我还研究了这在各种框架中有何不同,以及await
与 SynchronizationContext 相关的幕后关键字对我们的作用。
这是来自 MSDN 的有关 SynchronizationContext 的解释。
提供在各种同步模型中传播同步上下文的基本功能。此类实现的同步模型的目的是允许公共语言运行时的内部异步/同步操作在不同的同步模型下正常运行。此模型还简化了托管应用程序必须遵循的一些要求,以便在不同的同步环境下正常工作。
嗯,嗯?除了第一部分,这可能听起来像是一堆 gobbledygook,但我尝试提供有关此定义的更多上下文。
更多上下文的定义上面的解释试图说的是不同的框架在线程之间有不同的通信方式。我们需要这样做的原因有很多。其中之一可能是我们想在正确的上下文(读取线程)中调用特定代码。因此,Windows 窗体具有Control.BeginInvoke
或 WPF 具有Dispatcher.BeginInvoke
允许我们在从另一个线程调用线程的上下文中运行我们的代码。现在 SynchronizationContext 所做的是它充当所有这些的抽象,您可以说它充当一个抽象类,可用于表示不同框架中的当前位置。
SynchronizationContext 公开了几个虚拟方法,但现在让我们关注一下Post
。Post 接受一个委托,但确定何时何地运行该委托的责任在于它的实现。默认实现SynchronizationContext.Post
只是将它传递给ThreadPool
via QueueUserWorkItem
。但是框架可以从 SynchronizationContext 派生自己的上下文并覆盖该Post
方法。例如,Windows 窗体具有WindowsFormsSynchronizationContext
实现 Post 并将委托传递给Control.BeginInvoke
. WPF 有DispatcherSynchronizationContext
,它调用Dispatcher.BeginInvoke
.
但是前面的解释不是很有帮助。因为有些人首先不知道我们为什么需要Dispatcher.BeginInvoke
。所以我会尝试从另一个角度来解释它。这就是 SynchronizationContext 解决的实际问题。不是它在不同框架中取代或抽象的东西。
SynchronizationContext
是我们的代码正在运行的当前环境的表示。也就是说,在异步程序中,当我们将一个工作单元委托给另一个线程时,我们捕获当前环境并将其存储在一个实例中SynchronizationContext
并将其放置在 Task目的。重要的是我们捕获当前环境并将其传递给另一个线程。其他线程如何使用传入的上下文因框架而异,并非所有框架都需要这样做。我们会看到 Asp.Net Core 没有SynchronizationContext
. 这是Stackoverflow question的另一个很好的解释。
简而言之,SynchronizationContext 表示可能执行代码的“位置”。然后将在该位置调用传递给其 Send 或 Post 方法的委托。(Post 是 Send 的非阻塞/异步版本。)
何时以及为何需要捕获当前 SynchronicationContext 为什么需要它但重要的问题是,为什么我们需要在移动到另一个线程之前捕获当前位置或环境?答案是,可能有很多原因,也可能没有。一般的原因是我们需要一种在线程之间进行通信的方法。例如,也许我们需要在某个位置但在正确的线程中运行某个代码。想象一下,我们委托一段代码在另一个线程上运行并等待它。现在,无论 await 关键字之后的内容被传递给该线程,以作为我们继续块的一部分执行。看一下这两段代码。
try
{
decimal result = await CalculateAmountAsync();
Console.WriteLine(result);
}
catch (Exception error)
{
Console.WriteLine(error.Message);
}
Task calculationTask = CalculateAmountAsync();
calculationTask.ContinueWith(t =>
{
var errors = t.Exception as AggregateException;
if (errors == null)
{
Console.WriteLine(t.Result);
}
else
{
Exception actualException = errors.InnerExceptions.First();
Console.WriteLine(actualException.Message);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
上面代码中发生的情况是,await 之后的一些代码作为一个延续块执行。现在想象这个延续在另一个线程上运行。现在有些操作可以在其他线程上完成,有些则不能。事情是,对于那些不允许的操作,我们需要在调用线程的上下文中执行延续部分。这就是为什么我们需要调用者向另一个线程发出调用的位置的表示。我将举例说明为什么我们需要在 WPF 应用程序中这样做,以及如果我们不这样做会发生什么。
为什么在 WPF 应用程序中需要捕获当前上下文必要时的一个示例是在 WPF 应用程序中。假设我们将某种操作委托给另一个线程,并且我们想使用结果来设置文本框Text
属性。但问题是,在这个框架中,只有创建 UI 元素的线程才有权更改其属性。如果我们尝试从另一个线程更改 UI 元素,则会收到此错误。
这只是当我们想在另一个线程上做某事时可能发生的一个问题。另一个问题可能是我们的安全上下文,它允许在一个线程中进行一项操作,而不允许在另一个线程中进行操作。那么在这种情况下我们能做的就是在一个SynchronizationContext
实例中捕获当前运行的环境,并将其传递给另一个线程。
public static void DoWork()
{
//On UI thread
var sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate
{
// do work on ThreadPool
sc.Post(delegate
{
// do work on the original context (UI)
}, null);
});
}
在上面的代码中,我们捕获了当前上下文并对线程池做一些工作。完成后,我们需要继续执行程序的其余部分。但我们希望在正确的上下文中做到这一点。那是UI线程的工作环境,而不是线程池线程。所以我们将需要做的事情传递给我们的 Post 方法,SynchronizationContext
并且该代码在 UI 线程上运行。通过这样做,我们解决了对象的线程所有权问题。
但是一些依赖于其内部实现的框架不需要 SynchronizationContext。Asp.Net Core 就是这样一种框架。在此处阅读更多相关信息。简而言之,在这样的框架中,可能不需要 ConfigureAwait(false)。那是因为正在运行的线程没有 aSynchronizationContext.Current
并且它为空。如果当前上下文为空,那么无论如何它都会在线程池线程上运行。所以我们确实需要指示我们的程序不捕获上下文,因为没有。
另一个重要的事情是每个线程都有自己的 SynchronizationContext。这意味着如果我们将工作从一个线程池委托给另一个线程,我们可以获得当前运行环境的快照并将其传递给另一个线程。框架如何处理这些并不像了解 SynchronizationContext 是什么以及它对我们有什么作用那么重要。
async/await 对 SynchronizationContext 的作用知道当我们等待某事时会发生什么以及这个语法糖变成了什么是有益的。这大致就是我们等待某事时发生的情况。
await SomethingAsync();
RestOfMethod();
var task = SomethingAsync();
var currentSyncContext = SynchronizationContext.Current;
task.ContinueWith(delegate
{
if (currentSyncContext == null) RestOfMethod();
else currentSyncContext.Post(delegate { RestOfMethod(); }, null);
}, TaskScheduler.Current);
正如您在第一个摘录中看到的那样,我们只是等待该方法。但是,如果我们想对其进行脱糖(!),我们首先启动操作并获得一个任务承诺以供以后使用。如果存在,我们还会获取当前的 SynchronizationContext。现在任务继续执行,但在某些时候我们需要执行我们方法的其余部分。我们ContinueWith
通过为我们的方法的其余部分或我们的延续块使用和传递一个委托来做到这一点。
接下来如果SynchronizationContext为 null,则 RestOfMethod() 将在原来的 TaskScheduler(通常是 TaskScheduler.Default,即 ThreadPool)中执行。请注意,我们不会在调用线程上运行延续。因为我们没有发布我们方法的其余部分以在捕获的上下文上运行。如果 SynchronizationContext 为空,它可以是任何线程,它可以是运行它的线程池线程或 UI 线程。
如果 SynchronizationContext 不为 null,我们将传递我们方法的其余部分以在原始线程上运行。那是首先调用此任务的 UI 线程。我们通过将委托和我们的方法传递给 SynchronizationContext 的 post 方法来实现。
概括在这篇文章中,我解释了SynchronizationContext
它是什么以及它试图解决什么问题。我还更深入地探讨了为什么我们需要这个结构以及 .Net 如何在后台处理这些问题。