【问题标题】:async Task - What actually happens on the CPU?异步任务 - CPU 上实际发生了什么?
【发布时间】:2015-10-09 10:20:36
【问题描述】:

在询问this question 并看到我完全误解了这个概念后,我一直在阅读有关Tasks 的信息。诸如置顶答案herehere 之类的答案解释了idea,但我还是不明白。 所以我提出了一个非常具体的问题:在执行任务时,CPU 上实际发生了什么

这是我在阅读后所理解的:任务将与调用者共享 CPU 时间(假设调用者是“UI”),因此如果它是 CPU 密集型的 - 它会减慢 UI。如果任务不是 CPU 密集型 - 它将在“后台”运行。看起来很清楚……直到经过测试。下面的代码应该允许用户点击按钮,然后交替显示“Shown”和“Button”。但实际上:在“已显示”全部显示之前,表单一直处于忙碌状态(-不可能有用户输入)。

public Form1()
{
    InitializeComponent();
    Shown += Form1_Shown;
}

private async void Form1_Shown(object sender, EventArgs e)
{
    await Doit("Shown");
}

private async Task Doit(string s)
{
    WebClient client = new WebClient();
    for (int i = 0; i < 10; i++)
    {
        client.DownloadData(uri);//This is here in order to delay the Text writing without much CPU use.
        textBox1.Text += s + "\r\n";
        this.Update();//textBox1.
    }
}

private async void button1_Click(object sender, EventArgs e)
{
    await Doit("Button");
}

有人可以告诉我在执行任务时 CPU 上实际发生了什么(例如“当 CPU 不被 UI 使用时,任务会使用它,除非……等”)?

【问题讨论】:

  • Doit 同步运行。将方法标记为async Task 不会使其本身异步。事实上,我希望你看到一个编译警告,说明你有一个不等待的异步方法。
  • @DanielKelley 好的。那么我如何do让它异步运行呢?当然,除了使用DownloadDataAsync,这违背了这个测试的目的,因为我想创建自己的异步任务。
  • 好吧,在这种情况下,您应该等待异步的 DownloadDataAsync。 DownloadData 是同步的,因此会阻塞。
  • 或者您可以使用Task.Run 并等待它。
  • @EmpereurAiman 当有可用的异步选项时,这没有意义。它会不必要地在线程池线程上运行代码。

标签: c# .net multithreading asynchronous async-await


【解决方案1】:

理解这一点的关键是有两种任务 - 一种执行代码(我称之为委托任务),一种代表未来的事件(我称之为承诺任务)。这两个任务完全不同,尽管它们都由 .NET 中的 Task 实例表示。我有一些pretty pictures on my blog 可能有助于了解这些类型的任务有何不同。

委托任务是由Task.Run 和朋友创建的。它们在线程池上执行代码(如果您使用的是TaskFactory,则可能是另一个TaskScheduler)。大多数“任务并行库”文档都处理委托任务。这些用于在多个 CPU 之间传播受 CPU 限制的算法,或将受 CPU 限制的工作推离 UI 线程。

Promise 任务是由TaskCompletionSource&lt;T&gt; 和朋友(包括async)创建的。这些是用于异步编程的代码,非常适合 I/O 绑定代码。

请注意,您的示例代码将导致编译器警告,大意是您的“异步”方法Doit 实际上不是异步的,而是同步的。所以就目前而言,它将同步调用DownloadData,阻塞UI线程直到下载完成,然后它会更新文本框并最终返回一个已经完成的任务。 p>

要使其异步,您必须使用await

private async Task Doit(string s)
{
  WebClient client = new WebClient();
  for (int i = 0; i < 10; i++)
  {
    await client.DownloadDataTaskAsync(uri);
    textBox1.Text += s + "\r\n";
    this.Update();//textBox1.
  }
}

现在它在遇到await 时返回一个不完整的任务,这允许 UI 线程返回到它的消息处理循环。下载完成后,此方法的其余部分将作为消息排队到 UI 线程,并在处理该方法时继续执行该方法。当Doit方法完成后,它之前返回的任务就会完成。

因此,async 方法返回的任务在逻辑上代表该方法。任务本身是 Promise Task,而不是 Delegate Task,实际上并不“执行”。该方法分为多个部分(在每个await 点)并以块的形式执行,但任务本身不会在任何地方执行。

为了进一步阅读,我在how async and await actually work (and how they schedule the chunks of the method) 上有一篇博文,在why asynchronous I/O tasks do not need to block threads 上有另一篇博文。

【讨论】:

  • a) 谢谢。 b)我已经阅读了你的一些博客,所以也谢谢你。 c) 您代码中的DownloadDataTaskAsync 违背了它在我的代码中 的目的。因为我想要实现的不是下载任何东西,而是异步运行此代码:textBox1.Text += s + "\r\n";。 (我只是使用下载来获得文本更新之间的一些时间。)然后,我的问题是:如何使textBox1.Text += s + "\r\n"; 异步运行没有任何...Async 方法的帮助.使...Async 代码以它的方式运行的“魔力”是什么,所以我可以在我的代码中使用。
  • 也许我不够清楚。这就是我想做的事情:我希望Shown 事件处理程序每​​隔一段时间就开始写入TextBox,而无需离开该方法。同时,我希望能够单击一个按钮,该按钮将类似地写入 TextBox,以便在两者之间交替写入。而且我想只用 await 来做到这一点,not 使用任何内置的 …Async 方法魔术 - 我想学习如何使 这发生。跨度>
  • 我知道 DownloadDataTaskAsync 方法上的整个 await 不能满足 @ispiro 的需要,但是,为了完整起见,该代码的缺点是 10 次下载永远不会并行化。使用 Task.WhenAll 代替(请参阅我的答案中的选项 2)将允许这样做。
  • @ispiro:请参阅我的回答中提到的TaskCompletionSource&lt;T&gt;。可以这么说,这就是您实现“低级异步任务”的方式。关于every so often,如果需要延迟,请使用await Task.Delay(...)。或者使用带有TaskCompletionSource&lt;T&gt; 的计时器来实现您自己的延迟任务。
  • @ispiro:请注意,“异步运行textBox1 代码”实际上有意义。但是,您可以做的是在异步操作完成时运行该代码
【解决方案2】:

根据您链接的答案,任务和线程是完全不同的概念,您也对异步/等待感到困惑

Task 只是表示要完成的一些工作。它没有说明应该如何完成工作。

Thread 表示在 CPU 上运行的某些工作,但它与它一无所知的其他线程共享 CPU 时间。

您可以使用Task.Run()Thread 上运行Task。您的Task 将异步运行,并且独立于任何其他提供线程池线程的代码。

您还可以使用 async / await 在 SAME 线程上异步运行任务。每当线程遇到等待时,它都可以保存当前的堆栈状态,然后返回堆栈并继续其他工作,直到等待的任务完成。您的 Doit() 代码从不等待任何内容,因此将在您的 GUI 线程上同步运行直到完成。

【讨论】:

  • You can also run a Task asynchronously on the SAME thread using async / await. Anytime the thread hits an await, ... and carry on with other work until the awaited task has finished. - 那么如果任务没有运行,它会如何完成呢?或者您是说 Task 将在同一个线程上,但它 等待 的方法将在单独的线程上? (我不明白在一个线程上异步运行是什么意思。)
  • “我不明白在一个线程上异步运行是什么意思”。这就是你问题的症结所在。阅读 async/await。每个“等待”基本上都将代码切成块。每次线程等待尚未完成的事情时,它都可以离开并运行不同的块。该块可能是不同任务的一部分。
  • 这听起来就像单 CPU 计算机的多线程。但抛开语义不谈,这正是我认为我在测试代码中所做的事情——我让 UI 在我的代码等待 url 响应时运行。那么为什么 UI go away and run a different chunk 不接受用户点击按钮?
  • 因为正如我所说,您的 DoIt() 代码中没有等待,所以它永远没有机会消失。是的,它实际上就像旧的协作式多线程。
  • 但是Doit 是由await Doit(...); 调用的——这和await client.DownloadDataAsync(uri); 有什么区别?是什么让DownloadDataAsync 异步运行而我的代码却没有?
【解决方案3】:

任务使用ThreadPool,您可以广泛阅读它是什么以及它是如何工作的here

但简而言之,当一个任务被执行时,任务调度器会在ThreadPool 中查看是否有线程可用于运行该任务的动作。如果没有,它将排队,直到有一个可用。

ThreadPool 只是一组已经实例化的可用线程,因此多线程代码可以安全地使用并发编程,而不会一直因上下文切换而使 CPU 不堪重负。

现在,您的代码的问题在于,即使您返回 Task 类型的对象,您也没有同时运行任何东西 - 从未启动过单独的线程!

为了做到这一点,您有两个选择,或者您将Doit 方法作为任务启动,使用

选项1

Task.Run(() => DoIt(s));

这将在线程池中的另一个线程上运行整个DoIt 方法,但这会导致更多问题,因为在此方法中,您正在尝试访问 UI 控件。因此,您需要将这些调用编组到 UI 线程,重新考虑您的代码,以便在异步任务完成后直接在 UI 线程上完成 UI 访问。


选项 2(最好,如果可以的话)

您使用 已经异步的 .net API,例如 client.DownloadDataTaskAsync(); 而不是 client.DownloadData();

现在,就您而言,问题是您需要进行 10 次调用,这些调用将返回 10 个不同类型的 Task&lt;byte[]&gt; 对象,并且您希望等待所有对象完成,而不仅仅是一个.

为此,您需要创建一个List&lt;Task&lt;byte[]&gt;&gt; returnedTasks,并将DownloadDataTaskAsync() 的所有返回值添加到其中。然后,一旦完成,您就可以为您的 DoIt 方法使用以下返回值。

return Task.WhenAll(returnedTasks);

【讨论】:

  • 谢谢。但是正如我所写的,我读过这些东西并且不理解它。 CPU 上发生了什么? 如果您说 CPU 将在线程池中的线程上运行任务 - 那么请解释我的代码失败的原因。如果不是 - 我不认为这是我问题的答案。
  • 我编辑了我的回复,以进一步了解您的代码。
  • 谢谢。下载只是为了延迟文本而没有太多的 CPU 使用。我已经编辑了问题以使其更清楚。
  • 该示例的问题在于,您可以使用相同 API 的异步版本(我的选项 2),并且这始终是比我的选项更好的选择1. 但是,如果您没有这样的异步 API,例如 Thread.Sleep(),您可以随时使用我的选项 1。它是否回答了您的问题?
  • a) 感谢您的宝贵时间。 b)我不想下载任何东西。我只是使用它来在我的任务等待 I/O 时给 UI CPU 时间。 (但似乎我的错误是任务启动不正确。)
猜你喜欢
  • 2011-03-31
  • 2014-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-17
  • 2019-04-29
相关资源
最近更新 更多