目录
介绍
一点历史
实现
窗体
WPF和Silverlight
经典的ASP.NET
ASP.NET Core
在Action中的同步上下文
任务和同步上下文
在Action中配置等待
在WinForms中配置等待
在ASP.NET MVC中配置等待
最后说明
介绍同步上下文是线程运行的环境。它是定义线程如何响应消息的一组特征。将其视为定义线程边界的范围。如果不通过目标线程的同步上下文进行通信,任何线程都不能访问其他线程的数据或将任何工作单元传递给其他线程。值得一提的是,不同的线程可能共享同一个同步上下文,一个线程是否有一个同步上下文是可选的。
一点历史
如果您来自Windows开发背景,您绝对知道在WinForms中,例如,您无法从任何其他线程访问UI控件。您必须卸载您的任务,或者换句话说,通过Control.BeginInvoke()函数将您的代码(工作单元)委托给UI线程,或者更准确地说,使用ISynchronizeInvoke模式。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异步执行委托。
请注意,尽管默认情况下Send()和Post()行为不同,但在由平台实现时可能不会。现在让我们快速了解一下各种平台是如何实现同步上下文的。
窗体在Windows窗体应用程序中,同步上下文是通过WindowsFormsSynchronizationContext(System.Windows.Forms.dll:System.Windows.Forms)实现的,它本身派生自默认的SynchronizationContext( mscorlib.dll: System)。这个实现为Send()和Post()简单地分别调用Control.Invoke()和Control.BeginInvoke()。它们确保将工作单元发送到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中,同步上下文是通过DispatcherSynchronizationContext(WindowsBase.dll:System.Windows.Threading)实现的,它的作用与WinForms的对应物相同,它将委托的工作单元传递给UI线程执行,并在内部Dispatcher.Invoke()和Dispatcher.BeginInvoke()委托中使用。
经典的ASP.NET在Classic ASP.NET中,同步上下文是通过AspNetSynchronizationContext(System.Web.dll:System.Web)实现的,但是,它的实现方式与其他平台中的同步上下文不同。由于ASP.NET中没有UI线程的概念,而且每个请求都需要一个单独的线程来处理,所以AspNetSynchronizationContext只维护了一个未完成的操作队列,当所有的操作都完成后,这个请求就可以被标记为完成了。Post()仍将委派的工作传递给ThreadPool。
这是AspNetSynchronizationContext唯一的用法吗?不。AspNetSynchronizationContext最重要的任务是确保请求线程可以访问诸如HttpContext.Current和其他相关身份和文化数据之类的东西。你知道这个。有时您会在运行线程之前缓存对HttpContext.Current的引用,这时AspNetSynchronizationContext就派上用场了。
ASP.NET CoreASP.NET Core中没有AspNetSynchronizationContext等效项,它已被删除。为什么?它是应用于新平台的各种性能改进的一部分。它将应用程序从进入和离开同步上下文(我们将在稍后讨论)时产生的开销中释放出来。请在此处阅读有关ASP.NET Core的SynchronizationContext的更多信息。
在Action中的同步上下文现在,让我们看看正在运行的同步上下文。在这个例子中,我们将看到如何将工作单元从工作线程传递到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-Context Thread Test ”按钮。
由于代码通过SynchronizationContext.Send()将执行传递给主UI线程,我们现在可以看到调用者线程不同的结果,但是,工作已传递给主线程,线程1,它已成功处理代码。
任务和同步上下文同步上下文是async/await模式的核心部分之一。当您await执行任务时,您会暂停当前async方法的执行,直到给定任务的执行完成。
让我们深入了解上图的更多细节。await不只是等待工作线程完成!大致发生的事情是await在执行异步任务之前捕获当前同步上下文(离开当前同步上下文)。异步任务返回后,它再次引用原始同步上下文(重新进入同步上下文),其余方法继续。
让我们在action中查看。启动新项目或向现有项目添加新表单。转到新表单的设计器代码并更新以匹配以下内容:
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-context按钮并查看结果:
第二个选项通过调用SynchronizationContext.SetSynchronizationContext()简单地将同步上下文设置为UI线程同步上下文。这影响了我们的行为,当我们调用await时,它捕获当前同步上下文(即WinFormsSynchronizationContext),然后将当前上下文留给给定的任务并等待其完成。任务完成后,它再次重新进入当前上下文,并且您已经能够使用UI线程访问UI控件而无需任何委托或回调。
这里有一点注意,您可能会问自己为什么我们必须使用SetSynchronizationContext()?! await不应该自动捕获同步上下文?是的。但是当我们在新任务的上下文中运行(我们使用过Task.Run())时,它没有同步上下文。默认情况下,工作任务和线程没有同步上下文(您可以通过SynchronizationContext.Current检查 来调查这一点。)这就是为什么我们必须在调用Task.Run()之前首先引用UI上下文,然后我们必须使用SetSynchronizationContext()来设置它。在promise-style tasks和outside Task.Run()中,您可以使用该ConfigureAwait()选项,稍后解释。
在Action中配置等待同步上下文的核心概念之一是上下文切换。当您等待任务时会发生这种情况。您在等待任务之前捕获当前上下文,将其留给任务上下文,然后在任务完成时将其恢复(重新输入)。这个过程非常昂贵,并且在许多情况下,您不需要它!例如,如果您在任务之后没有处理UI控件,为什么要再次切换到原始上下文?你为什么不节省时间并避免这一轮?
经验法则是,如果您正在开发一个库,或者您不需要访问UI控件,或者您可以引用同步数据(如HttpContext.Current)以供以后使用,那么可以节省您的时间和精力并禁用上下文切换。
这里Task.ConfigureAwait()就派上用场了。它有一个参数continueOnCapturedContext,如果设置为true,则启用上下文恢复(如果ConfigureAwait()未使用,则为默认行为)或在设置为false时禁用它。
让我们在action中查看。
在WinForms中配置等待启动一个新的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中配置等待启动一个新的MVC项目,并更新index.cshtml文件以匹配以下内容:
@model IEnumerable
@if (null != Model && Model.Any())
{
@foreach (var val in Model)
{
- @val
}
}
现在,转到Home控制器并添加以下代码:
private List results = new List();
public async Task 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上获得:
- GitHub - elsheimy/Samples.SynchronizationContext: Synchronization Context Samples
https://www.codeproject.com/Articles/5311504/Understanding-Synchronization-Context-Task-Configu