异步任务 - CPU上实际发生了什么?

时间:2015-10-09 10:20:36

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

我在询问this question之后一直在阅读关于Task的内容,并且发现我完全误解了这个概念。顶部答案herehere等答案解释了的想法,但我仍然没有得到它。 所以我已经提出了一个非常具体的问题:当执行任务时,实际上 在CPU 上发生了什么?

这是我在阅读之后所理解的:一个任务将与调用者共享CPU时间(让我们假设调用者是“UI”),这样如果它是CPU密集型的 - 它将减慢UI。如果任务 CPU密集型 - 它将“在后台”运行。似乎足够清楚......直到测试。以下代码应该允许用户单击按钮,然后交替显示“已显示”和“按钮”。但实际上:表格完全忙碌(没有用户输入),直到“显示”全部显示。

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上发生了什么(例如“当UI不使用CPU时,任务使用它,除了等等......”)?

3 个答案:

答案 0 :(得分:3)

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

委托任务是Task.Run和朋友创建的任务。它们在线程池上执行代码(如果您使用的是TaskScheduler,则可能执行另一个TaskFactory)。大多数“任务并行库”文档都涉及委托任务。它们用于在多个CPU之间传播CPU绑定算法,或者从UI线程推送CPU绑定工作。

承诺任务是TaskCompletionSource<T>和朋友(包括async)创建的任务。这些是用于异步编程的,并且非常适合I / O绑定代码。

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

要使其异步,您必须使用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上有另一篇博文。

答案 1 :(得分:0)

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

但简而言之,当执行任务时,任务计划程序会查找ThreadPool以查看是否有可用于运行任务操作的线程。如果没有,它将排队,直到有一个可用。

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

现在,代码的问题在于,即使您返回类型为ThreadPool的对象,也没有同时运行任何内容 - 没有单独的线程可以启动!

为了做到这一点,您有两种选择,要么将Task方法作为任务启动,请使用

选项1

Doit

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

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

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

现在,在您的情况下,问题是您将需要进行10次调用,这些调用将返回类型为client.DownloadData();的10个不同对象,并且您希望等待所有这些对象完成,而不是只有一个。

要执行此操作,您需要创建Task<byte[]>,然后您将从List<Task<byte[]>> returnedTasks添加所有返回值。然后,一旦完成,您可以使用以下DownloadDataTaskAsync()方法的返回值。

DoIt

答案 2 :(得分:0)

根据你的链接答案,任务和线程是完全不同的概念,你也对async / await感到困惑

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

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

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

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