为什么异步回调在WPF UI线程中执行

7

关于这段代码:

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**

这是可以的:等待后面的代码片段在任务本身所在的同一线程中执行。

现在,如果我在一个 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中硬编码?
感谢您提供的任何想法。
3个回答

7
原因是Task.Run将会捕获SynchronizationContext,如果在UI线程中启动任务时存在该上下文,比如在WPF应用程序中。任务将使用SynchronizationContext来序列化回调到UI线程。然而,如果没有上下文可用,比如在控制台应用程序中,则回调将在不同的线程上发生。
Stephen Toub在博客文章中描述了这一点。
顺便说一句,在任务中使用Thread.Sleep要小心,甚至不要使用。这可能会导致奇怪的行为,因为任务可能不会绑定到一个线程。请改用Task.Delay。

2

但是我想知道“为什么”?

你已经回答了这个问题:

好吧,这很棒,因为它使操作可观察的集合变得更加容易...

异步的整个重点是使异步操作更加容易处理,这样你就可以编写看起来“同步”的代码,实际上是异步的。通常包括希望在整个异步方法中保持一个上下文(例如UI线程),当需要等待某些东西时,只需“暂停”方法(而不会阻塞UI线程)。

“我该如何做到这一点?”

不清楚你的意思。基本上,Task 的 awaitable 模式的实现使用 TaskScheduler.FromCurrentSynchronizationContext() 来确定要将回调发布到哪个计划程序上,除非你已经调用了ConfigureAwait(false) 以明确选择退出此行为。所以这就是它是如何处理的...你是否可以“做到相同”,取决于你要做什么。

有关 awaitable 模式的更多详细信息,请参见async/await FAQ 中的“什么是 awaitables”问题。


“如何才能做到相同的效果?”我认为他的意思是:“如何在不使用WPF的情况下获得WPF的行为?” - user180326
如果那是意思的话,它非常不清楚。无论如何,希望我提供的细节足以为OP提供足够的信息。如果需要,他们总是可以要求更多的细节。 - Jon Skeet
好的,没问题。我并不想做同样的事情。我在想回调函数是如何发送到Dispatcher.Invoke方法的...如果我可以,最终是否能够做类似的事情,或者这是在框架中硬编码的。谢谢你的回答。 - Kek

0

你可能会发现我的 async / await入门有所帮助。其他答案几乎是正确的。

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

请注意,此操作需要满足以下条件:

  • "当你使用await时..." - 如果你手动安排一个继续执行的方法,例如使用Task.ContinueWith,那么就不会进行上下文捕获。你需要使用类似于(SynchronizationContext.Current == null ? TaskSchedler.Current : TaskScheduler.FromCurrentSynchronizationContext())来进行自己的上下文捕获。
  • "...等待一个Task时..." - 这种行为是Task类型的await行为的一部分。其他类型可能会或可能不会进行相似的捕获。
  • "...尚未完成..." - 如果Taskawait之前已经完成,那么async函数将会以同步方式继续执行。因此,在这种情况下没有必要进行上下文捕获。
  • "...默认情况下..." - 这是默认的行为,可以更改。特别是调用Task.ConfigureAwait方法,并将continueOnCapturedContext参数设置为false。此方法返回一个可等待的类型(而不是Task),如果它的参数为false,则不会在捕获的上下文中继续执行。

谢谢您提供这个完整的答案。其中大部分(或者至少足以回答我的问题)都在@Jakob Christensen的答案链接中,但我很感激这个全面的观点! - Kek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接