.net 切换同步上下文是否意味着工作将在拥有同步上下文的线程上运行?

.net 切换同步上下文是否意味着工作将在拥有同步上下文的线程上运行?,.net,multithreading,asynchronous,task-parallel-library,synchronizationcontext,.net,Multithreading,Asynchronous,Task Parallel Library,Synchronizationcontext,无论您是运行任务,还是不运行类似的任务: 或者在旧方法的基础上使用,在旧方法的基础上调用InvokeRequired以检查是否需要在另一个同步上下文上调用当前操作,然后调用控件。例如,在WinForms的情况下,如果存在捕获的同步上下文,则使用捕获的同步上下文执行操作 然而,这意味着这两件事中的哪一件 如果使用任何方法(旧方法或新方法)请求在线程池线程上运行任务,是否意味着: 当线程启动时,同步上下文的切换意味着它将等待拥有同步上下文的线程执行这段代码?在UI拥有的同步上下文的情况下,这是否意味

无论您是运行任务,还是不运行类似的任务:

或者在旧方法的基础上使用,在旧方法的基础上调用InvokeRequired以检查是否需要在另一个同步上下文上调用当前操作,然后调用控件。例如,在WinForms的情况下,如果存在捕获的同步上下文,则使用捕获的同步上下文执行操作

然而,这意味着这两件事中的哪一件

如果使用任何方法(旧方法或新方法)请求在线程池线程上运行任务,是否意味着:

当线程启动时,同步上下文的切换意味着它将等待拥有同步上下文的线程执行这段代码?在UI拥有的同步上下文的情况下,这是否意味着线程池线程将把操作发回UI线程的消息队列并产生响应

或者,这是否意味着线程池线程将执行该操作,但只会有一个包含同步上下文的引用变量System.Threading.ExecutionContext.SynchronizationContext的开关,因此,同步上下文只是线程同意遵守的一个协作规程?工作仍将由线程池线程完成

当然,通过合作原则*,我并不意味着即使恶意线程决定不在需要的地方切换同步上下文,所有事情都会正常工作。我的意思是,如果synchronization上下文引用更改为正确的引用,那么最初不拥有同步上下文的线程仍然可以运行

通过阅读的源代码和方法,答案很可能是2,但我不确定

更新

这也是为什么我认为2更有可能,但我希望得到纠正

如果您只是使用Windows窗体应用程序并在其上粘贴一个按钮,然后运行单击事件中图片中显示的代码,您可能会看到与我的调用堆栈类似的调用堆栈,如同一图片所示

我遵循了调用堆栈中每个方法的源代码。我注意到上下文切换发生在System.Threading.ExecutionContext.RunInternal方法中。之所以发生这种情况,是因为System.Threading.Tasks.ExecuteWithThreadLocal方法将其调用System.Threading.ExecutionContext.Run方法的最后一个参数的值传递为true。请看

但是,此后,调用将继续进行,而不向UI线程的消息队列发送任何消息,当它最终到达System.Threading.Tasks.Task.InnerInvoke方法时,该方法将调用委托

如果答案是1,如果你能告诉我消息发布的地点,我会高兴得跳起来,并且今天会学到一些关于同步上下文的有趣的东西

它是否发生在ExecutionContext.SetExecutionContext方法中

如果答案是2,如果你能证实这一点,那么我也会唱一首歌来庆祝我的发现

旁注

不过,我制作这个程序是为了测试一些不同的东西。我想 查看同步上下文的切换位置(如果是) 必需,两个:

在到达等待声明之前;和 在wait语句之后,即在continuation回调上。 我的发现令人满意地向我揭示了这两个问题的答案 这些问题

对于任何好奇的人来说,这个开关是在AsyncMethodBuilder的 等待表达式之前的任何代码的Start方法

对于后面的代码,有多条路径。其中一条路 在上图所示的调用堆栈中描述了

我有一个解释await如何与SynchronizationContext.Current一起工作的示例。具体地说,wait使用捕获的上下文来恢复异步方法

因此,这是不正确的:

如果存在同步上下文,则使用捕获的同步上下文执行该操作

如果说手术,你是指。。。在代码中:

public async void button1_click(...)
{
  await Task.Run(...);
}
将发生的是该任务。运行将计划。。。到线程池线程任务。运行始终使用线程池。wait然后捕获当前SynchronizationContext(在本例中为UI上下文)并返回。什么时候完成,则task.Run返回的任务将完成,并且按钮1\u单击将在UI线程的该上下文上继续。然后到达方法的末尾并返回

在…中,SynchronizationContext.Current将为null。wait设置的任务延续使用其捕获的SynchronizationContext在UI线程上继续。

我有一个例子解释了wait如何与SynchronizationContext.Current一起工作。具体地说,wait使用捕获的上下文来恢复异步方法 d

因此,这是不正确的:

如果存在同步上下文,则使用捕获的同步上下文执行该操作

如果说手术,你是指。。。在代码中:

public async void button1_click(...)
{
  await Task.Run(...);
}
将发生的是该任务。运行将计划。。。到线程池线程任务。运行始终使用线程池。wait然后捕获当前SynchronizationContext(在本例中为UI上下文)并返回。什么时候完成,则task.Run返回的任务将完成,并且按钮1\u单击将在UI线程的该上下文上继续。然后到达方法的末尾并返回


在…中,SynchronizationContext.Current将为null。wait设置的任务延续使用捕获的SynchronizationContext在UI线程上恢复。

从阅读更多代码和思考更多内容来看,答案很可能是2

以下是这一主张的理由

仅仅因为工作线程不拥有需要在其上执行操作的同步上下文,就让工作线程从CPU中抽空并导致上下文切换,这将是非常昂贵的

同步上下文只是一个对象,它包含线程要对其执行操作的窗口或资源的句柄。如果当前正在执行的线程没有该同步上下文对象,那么将引用复制到拥有资源句柄的同步上下文,并让线程池线程使用该上下文运行,将更加省钱。这个拷贝必须在线程在CPU上就位之前完成;换句话说,在开始操作之前,它仍在就绪队列中

[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
    if (((TStateMachine) stateMachine) == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref ecsw);
        stateMachine.MoveNext();
    }
    finally
    {
        ecsw.Undo();
    }
}
还必须注意的是,在复制期间不会进行新的堆分配,因为这样做也会造成浪费。仅复制对拥有资源的同步上下文的引用,并将其设置为当前线程的上下文

NET framework源代码中的以下代码片段支持了我的这一理论

AsyncTaskMethodBuilder.Start调用ExecutionContextReader来复制拥有资源的执行上下文,完成后,它调用ExecutionContextSwitcher上的Undo,从而反转上一个操作

[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
    if (((TStateMachine) stateMachine) == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref ecsw);
        stateMachine.MoveNext();
    }
    finally
    {
        ecsw.Undo();
    }
}
因此,不会将任何工作发回UI线程的消息队列。相反,工作线程在拥有资源句柄的环境/同步上下文中执行工作


这意味着同步上下文是一个协作规程,因为它只是一个协议,用于将拥有操作系统句柄的堆对象复制到线程在执行时希望用于执行的资源。

从阅读更多代码和思考更多代码,答案很可能是2

以下是这一主张的理由

仅仅因为工作线程不拥有需要在其上执行操作的同步上下文,就让工作线程从CPU中抽空并导致上下文切换,这将是非常昂贵的

同步上下文只是一个对象,它包含线程要对其执行操作的窗口或资源的句柄。如果当前正在执行的线程没有该同步上下文对象,那么将引用复制到拥有资源句柄的同步上下文,并让线程池线程使用该上下文运行,将更加省钱。这个拷贝必须在线程在CPU上就位之前完成;换句话说,在开始操作之前,它仍在就绪队列中

[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
    if (((TStateMachine) stateMachine) == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref ecsw);
        stateMachine.MoveNext();
    }
    finally
    {
        ecsw.Undo();
    }
}
还必须注意的是,在复制期间不会进行新的堆分配,因为这样做也会造成浪费。仅复制对拥有资源的同步上下文的引用,并将其设置为当前线程的上下文

NET framework源代码中的以下代码片段支持了我的这一理论

AsyncTaskMethodBuilder.Start调用ExecutionContextReader来复制拥有资源的执行上下文,完成后,它调用ExecutionContextSwitcher上的Undo,从而反转上一个操作

[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
    if (((TStateMachine) stateMachine) == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref ecsw);
        stateMachine.MoveNext();
    }
    finally
    {
        ecsw.Undo();
    }
}
因此,不会将任何工作发回UI线程的消息队列。相反,工作线程在拥有资源句柄的环境/同步上下文中执行工作


这意味着同步上下文是一个协作规程,因为它只是一个协议,用于将拥有操作系统句柄的堆对象复制到线程在执行时希望用于执行的资源。

您的假设不正确,Control.Begin/Invoke不使用同步上下文。反过来说,使用Control.Begin/Invoke的是SynchronizationContext。特别是WindowsFormsSynchronizationContext实例,当

无论您创建控件还是调用Application.Run。你的问题在基本细节准备就绪后就没有意义了。@HansPassant我正在考虑你说的话。@HansPassant我认为你的更正是有好处的。我对事物的知识有限,这是不可原谅的。我不知道如何才能立即解决这个问题,并重新表述我的问题。我的问题的要点是:在线程池线程执行操作之前,如果需要捕获同步上下文,这是否会迫使线程池线程放弃其CPU时间片并将其工作委托回UI线程?或者,这是否意味着线程池线程将在执行操作之前更改保存同步上下文的引用变量的值?不会发生任何戏剧性的变化,它只是复制SynchronizationContext.current的值。@HansPassant非常感谢。我怀疑是这样。您想把它作为一个答案吗?您的假设不正确,Control.Begin/Invoke不使用同步上下文。反过来说,使用Control.Begin/Invoke的是SynchronizationContext。特别是WindowsFormsSynchronizationContext实例,该实例在创建控件或调用Application.Run时自动安装。你的问题在基本细节准备就绪后就没有意义了。@HansPassant我正在考虑你说的话。@HansPassant我认为你的更正是有好处的。我对事物的知识有限,这是不可原谅的。我不知道如何才能立即解决这个问题,并重新表述我的问题。我的问题的要点是:在线程池线程执行操作之前,如果需要捕获同步上下文,这是否会迫使线程池线程放弃其CPU时间片并将其工作委托回UI线程?或者,这是否意味着线程池线程将在执行操作之前更改保存同步上下文的引用变量的值?不会发生任何戏剧性的变化,它只是复制SynchronizationContext.current的值。@HansPassant非常感谢。我怀疑是这样。请你记下来作为回答好吗?谢谢。我并不好奇等待的操作或延续将在什么样的同步上下文中运行。这两个我都学到了。通过遵循System.Threading.Tasks.Task.SetContinuationForWait方法的源代码,我能够确定继续在正确的同步上下文上运行。通过这个问题,我能够了解线程池线程委托中上下文的值。我问题的要点是:当线程池线程需要使用UI线程的同步上下文运行操作时,例如,是否使用TPL,这是否意味着线程池线程将把委托发布到UI线程的消息队列中,以便后者执行委托并产生CPU时间的一部分?或者,这是否意味着线程池线程将执行委托,但只是在执行之前更改保存同步上下文的引用变量?顺便说一句,我已经阅读了您链接到的介绍文章,并且在您的其他文章中很喜欢它。我打算完整地阅读您的博客。@WaterCoolerv2:对于UI线程,它必须是发布到消息队列的代理。不可能让另一个线程临时成为UI线程。谢谢。那很好。现在我要去挖掘一些代码,我之前看到的代码和你刚才说的有点不一致。对不起,我不是故意刁难你。我只想一劳永逸地在我的脑海里把这件事摆正,这样我就可以继续做其他事情了。谢谢。我并不好奇等待的操作或延续将在什么样的同步上下文中运行。这两个我都学到了。通过遵循System.Threading.Tasks.Task.SetContinuationForWait方法的源代码,我能够确定继续在正确的同步上下文上运行。通过这个问题,我能够了解线程池线程委托中上下文的值。我问题的要点是:当线程池线程需要使用UI线程的同步上下文运行操作时,例如,是否使用TPL,这是否意味着线程池线程将把委托发布到UI线程的消息队列中,以便后者执行委托并产生CPU时间的一部分?或者,这是否意味着线程池线程将执行委托,但只是在执行之前更改保存同步上下文的引用变量?顺便说一句,我已经阅读了您链接到的介绍文章,并且在您的其他文章中很喜欢它。我打算完整地阅读您的博客。@WaterCoolerv2:对于UI线程,它必须是发布到消息队列的代理。不可能让另一个线程临时成为UI线程。谢谢。那很好。现在我要去挖掘一些我之前看到的代码,这些代码与您的代码有点不一致
我刚才说。对不起,我不是故意刁难你。我只想一劳永逸地把这件事记在脑子里,这样我就可以继续做其他事情了。我会根据我对此事的进一步调查结果更新我的答案。看来诉讼程序并不像我目前的回答那样简单。我将根据我对此事的进一步调查结果更新我的回答。看来,诉讼程序并不像我目前的回答所表明的那样直接。