【问题标题】:Why are Async callback executed in WPF UI thread为什么在 WPF UI 线程中执行异步回调
【发布时间】:2013-04-25 06:02:27
【问题描述】:

关于这段代码:

static async Task<string> testc()
{
    Console.WriteLine("helo async " + Thread.CurrentThread.ManagedThreadId); 
    await Task.Run(() => { 
        Thread.Sleep(1000);
        Console.WriteLine("task " + Thread.CurrentThread.ManagedThreadId); 
    });
    Console.WriteLine("callback "+Thread.CurrentThread.ManagedThreadId);
    return "bob";
}

static void Main(string[] args)
{
    Console.WriteLine("helo sync " + Thread.CurrentThread.ManagedThreadId);
    testc();
    Console.WriteLine("over" + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(2000);
    Console.ReadLine();
}

我得到以下输出:

helo sync 10
helo async 10
over10
task 11
callback **11**

这没关系:await 之后的一段代码与任务本身在同一个线程中执行。

现在,如果我在 WPF 应用程序中执行此操作:

private void Button_Click_1(object sender, RoutedEventArgs e)
{
    Console.WriteLine("helo sync " + Thread.CurrentThread.ManagedThreadId);
    testc();
    Console.WriteLine("over" + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(2000);
    Console.ReadLine();
}

它生成以下输出:

helo sync 8
helo async 8
over8
task 9
callback **8**

我们可以看到在 UI 线程中执行 await 后的代码。好吧,这很棒,因为它可以操纵可观察的集合等……但我想知道“为什么?” “我怎么能做同样的事?”这与某些 TaskScheduler 行为有关吗?这是在 .NET Framework 中硬编码的吗?

感谢您提交的任何想法。

【问题讨论】:

    标签: c# wpf asynchronous


    【解决方案1】:

    原因是当从 UI 线程启动任务时,Task.Run 将捕获SynchronizationContext,如果它存在于 WPF 应用程序中。然后,任务将使用SynchronizationContext 将回调序列化到 UI 线程。但是,如果在控制台应用程序中没有可用的上下文,则回调将发生在不同的线程上。

    Stephen Toub 在blog entry 中对此进行了描述。

    顺便说一句,使用时要小心永远不要在任务中使用 Thread.Sleep。它可能会导致奇怪的行为,因为任务可能未绑定到一个线程。请改用 Task.Delay。

    【讨论】:

      【解决方案2】:

      但我想知道“为什么?”

      你自己已经回答了:

      嗯,这很棒,因为它可以操作可观察的集合等......

      异步的全部意义在于使异步更易于使用 - 因此您可以编写实际上是异步的“看似同步”的代码。这通常包括希望将整个 async 方法停留在一个上下文(例如 UI 线程)中 - 当您需要等待某事时,只需“暂停”该方法(不阻塞 UI 线程)。

      “我怎么能这样做?”

      不清楚你在这里的意思。基本上,Task 的可等待模式的实现使用 TaskScheduler.FromCurrentSynchronizationContext() 来确定要在哪个调度程序上发布回调 - 除非您已调用 ConfigureAwait(false) 以明确选择退出此行为。所以它就是这样管理它的......你是否可以“做同样的事情”取决于你想要做什么。

      有关可等待模式的更多详细信息,请参阅async/await FAQ 中的“什么是可等待对象”问题。

      【讨论】:

      • “我怎么能这样做?”我认为他的意思是:“如何在不使用 WPF 的情况下获得 WPF 行为?”
      • @jdv-JandeVaan:如果是这个意思,那就非常不清楚了。无论如何,希望我提供的详细信息将提供足够的信息来进一步推动 OP。如有必要,他们可以随时询问更多详细信息。
      • 好的..很好。我真的不想做同样的事情。我想知道如何将回调发送到 Dispatcher.Invoke 方法......如果我最终可以做类似的事情,或者它是否在框架中被硬编码。谢谢你的回答
      【解决方案3】:

      您可能会发现我的async/await intro 很有帮助。其他答案几乎正确。

      当您 await 一个尚未完成的 Task 时,默认情况下会捕获一个“上下文”,用于在 Task 完成时恢复该方法。这个“上下文”是SynchronizationContext.Current除非它为空,在这种情况下它是TaskScheduler.Current

      注意此工作所需的条件:

      • “当您await...” - 如果您手动安排继续,例如使用Task.ContinueWith,则不会进行上下文捕获。您必须自己使用 (SynchronizationContext.Current == null ? TaskSchedler.Current : TaskScheduler.FromCurrentSynchronizationContext()) 之类的东西来完成。
      • “...await a Task...” - 此行为是await 行为的一部分对于Task 类型。其他类型可能会也可能不会进行类似的捕获。
      • “...尚未完成...” - 如果Taskawaited 时已经完成,async 方法将继续同步。因此,在这种情况下无需捕获上下文。
      • “...默认情况下...” - 这是默认行为,可以更改。特别是,调用Task.ConfigureAwait 方法并将false 传递给continueOnCapturedContext 参数。如果其参数为 false,则此方法返回一个等待类型(不是 Task),该类型将不会在捕获的上下文中继续。

      【讨论】:

      • 感谢这个完整的答案。其中大部分(或至少足以回答我的问题)都在@Jakob Christensen 的答案中的链接中,但我很欣赏这个完整的观点!
      猜你喜欢
      • 2014-04-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-05
      • 2023-03-12
      • 1970-01-01
      相关资源
      最近更新 更多