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

时间:2016-06-06 20:34:41

标签: .net multithreading asynchronous task-parallel-library synchronizationcontext

是否运行任务(和不是Task ),如下所示:

public async void button1_click(...)
{
    await Task.Run(...);
}

或使用旧方法调用InvokeRequired来检查是否需要在另一个同步上下文中调用当前操作,然后调用Control.Invoke(在WinForms的情况下)例如,如果有一个,则使用捕获的同步上下文执行操作。

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

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

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

    或者

  2. 或者它是否意味着线程池线程将执行操作,但只会有一个包含同步上下文的引用变量(System.Threading.ExecutionContext.SynchronizationContext)的切换,因此,同步上下文线索同意遵守的只是一个合作学科?这项工作仍将由线程池线程完成?

    当然,通过合作纪律*,我并不是说即使错误的线程决定不在需要时切换同步上下文,一切都会正常工作。我的意思是,如果同步上下文引用更改为正确的,则最初不拥有同步上下文的线程仍然可以运行。

  3. 通过阅读AsyncMethodBuilder<TResult>.StartSystem.Threading.Tasks.Task.ExecuteWithThreadLocalSystem.Threading.ExecutionContext.RunInternal方法的源代码,答案很可能是#2,但我不确定。

    更新

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

    如果您只是使用Windows窗体应用程序并在其上粘贴一个按钮并运行click事件中图片中显示的代码,您可能会看到看起来像我的调用堆栈,如同一张图片所示。

    enter image description here

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

    然而,此后,调用继续进行,而没有任何消息发布到UI线程的消息队列,并且当它最终到达System.Threading.Tasks.Task<TResult>.InnerInvoke方法时,该方法将调用该委托。

    如果答案是#1,如果你能告诉我发布消息的位置,我会高兴地跳起来,并且今天会学到一些关于同步环境的精彩内容。

    是否在ExecutionContext.SetExecutionContext方法中发生了?

    如果答案是#2,如果你能确认,那么,我也会唱一首歌来庆祝我的发现。

      

    旁注

         

    我制作了这个程序来测试不同的东西。我想   请参阅其中切换同步上下文(如果是)   需要,两者:

         
        
    1. 在达到await陈述之前;和
    2.   
    3. await语句之后,即延续回调
    4.         

      我的发现令人满意地向我揭示了两者的答案   问题。

           

      对于任何好奇的人,都会在AsyncMethodBuilder中进行切换    Start表达式之前的任何代码的await方法。

           

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

2 个答案:

答案 0 :(得分:3)

我有一个async intro blog post,解释await如何与SynchronizationContext.Current一起使用。具体而言,await使用捕获的上下文恢复 async方法。

所以,这是不对的:

  

使用捕获的同步上下文执行操作(如果有)。

如果通过“操作”,则表示代码中的...

public async void button1_click(...)
{
  await Task.Run(...);
}

将会发生Task.Run...安排到线程池线程(Task.Run始终使用线程池)会发生什么。然后await捕获当前SynchronizationContext(在本例中为UI上下文)并返回。 ...完成后,Task.Run返回的任务将完成,button1_click将在该上下文(UI线程)上恢复。然后它到达方法的末尾并返回。

...内,SynchronizationContext.Current将为null。这是由await设置的任务延续,它使用其捕获的SynchronizationContext在UI线程上恢复。

答案 1 :(得分:1)

通过阅读更多代码并思考更多代码,答案似乎很可能是#2。

这是此声明的原因。

让工作线程撤离CPU并导致上下文切换只是因为它不拥有需要执行操作的同步上下文,这将是非常昂贵的。

同步上下文只是一个对象,它包含线程想要对其执行操作的窗口或资源的HANDLE。如果当前正在执行的线程没有该同步上下文对象,那么复制对 拥有资源句柄并让线程池线程运行的同步上下文的引用会更加节俭那个背景。必须在线程在CPU上取代之前进行此复制;换句话说,它在开始运行之前仍处于就绪队列中。

还必须注意,在复制期间不会进行新的堆分配,因为它们也会浪费。仅复制对拥有同步上下文的资源的引用,并将其设置为当前线程的上下文。

我的这个理论得到了.NET框架源代码片段中的以下代码片段的支持。

AsyncTaskMethodBuilder<TResult>.Start调用ExecutionContextReader来复制拥有该资源的执行上下文,完成后,它会调用Undo上的ExecutionContextSwitcher,这会反转之前的操作

[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();
    }
}

System.Threading.Tasks.Task.ExecuteWithThreadLocal方法使用捕获的执行上下文调用ExecutionContext.Run,要求它保留捕获的执行上下文。

Run方法调用ExecutionContext.RunInternal,它执行相同的操作:创建捕获的上下文的副本,并将其设置为当前正在运行的线程的执行上下文。完成后,它会调用ExecutionContextSwitcher.Undo方法来撤消先前的复制操作并恢复线程的原始执行上下文。

[SecurityCritical, HandleProcessCorruptedStateExceptions]
internal static void RunInternal(ExecutionContext executionContext, ContextCallback callback, object state, bool preserveSyncCtx)
{
    if (!executionContext.IsPreAllocatedDefault)
    {
        executionContext.isNewCapture = false;
    }
    Thread currentThread = Thread.CurrentThread;
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        Reader executionContextReader = currentThread.GetExecutionContextReader();
        if ((executionContextReader.IsNull || executionContextReader.IsDefaultFTContext(preserveSyncCtx)) && ((SecurityContext.CurrentlyInDefaultFTSecurityContext(executionContextReader) && executionContext.IsDefaultFTContext(preserveSyncCtx)) && executionContextReader.HasSameLocalValues(executionContext)))
        {
            EstablishCopyOnWriteScope(currentThread, true, ref ecsw);
        }
        else
        {
            if (executionContext.IsPreAllocatedDefault)
            {
                executionContext = new ExecutionContext();
            }
            ecsw = SetExecutionContext(executionContext, preserveSyncCtx);
        }
        callback(state);
    }
    finally
    {
        ecsw.Undo();
    }
}

因此,没有工作回发到UI线程的消息队列。相反,工作线程在拥有资源句柄的环境/同步上下文中执行工作。

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