开始和等待任务有什么区别?

时间:2018-10-21 20:31:20

标签: c# asynchronous async-await task-parallel-library

开始和等待之间有什么区别?下面的代码摘自Stephen Cleary的博客(包括评论)

public async Task DoOperationsConcurrentlyAsync()
{
  Task[] tasks = new Task[3];
  tasks[0] = DoOperation0Async();
  tasks[1] = DoOperation1Async();
  tasks[2] = DoOperation2Async();

  // At this point, all three tasks are running at the same time.

  // Now, we await them all.
  await Task.WhenAll(tasks);
}

我认为任务在您等待时就开始运行...但是代码中的注释似乎暗示了其他情况。 另外,在将任务归因于Task类型的数组之后,任务如何运行。从本质上讲,这不只是一种归因,不涉及行动吗?

4 个答案:

答案 0 :(得分:1)

Task返回“ hot”(即已经启动)。 await异步等待Task完成。

在您的示例中,实际执行await的位置将影响任务是一个接一个地运行,还是同时运行所有任务:

await DoOperation0Async(); // start DoOperation0Async, wait for completion, then move on
await DoOperation1Async(); // start DoOperation1Async, wait for completion, then move on
await DoOperation2Async(); // start DoOperation2Async, wait for completion, then move on

相对于:

tasks[0] = DoOperation0Async(); // start DoOperation0Async, move on without waiting for completion
tasks[1] = DoOperation1Async(); // start DoOperation1Async, move on without waiting for completion
tasks[2] = DoOperation2Async(); // start DoOperation2Async, move on without waiting for completion

await Task.WhenAll(tasks); // wait for all of them to complete

更新

  

await不会执行async操作...在此示例中(不仅是)表现得像同步一样吗?因为我们不能(!)与其他命令并行运行DoOperation0Async()在第一种情况下,相比之下,在第二种情况下,DoOperation0Async()DoOperation1Async()并行运行(例如,并发性,async的主要好处?)”

这是一个很大的主题,值得一提的一个问题,因为它是SO上的专有线程,因为它偏离了开始和等待任务之间的区别的原始问题-因此,我将简短回答这个问题,同时将您介绍给其他人适当的答案。

否,await执行async操作不会使其表现得像同步;这些关键字的作用是使开发人员能够编写类似于同步工作流程的异步代码(有关更多信息,请参见Eric Lippert的this answer)。

调用await DoOperation0Async() 不会阻塞执行此代码流的线程,而DoOperation0(或类似DoOperation0Async.Result之类的同步版本)会阻塞线程,直到操作完成。

在网络环境中考虑这一点。假设有一个请求到达服务器应用程序。在生成对该请求的响应的过程中,您需要执行长时间的操作(例如,查询外部API以获取生成响应所需的一些值)。如果此长时间运行的操作是同步执行的,则执行您的请求的线程将 block ,因为它必须等待长时间运行的操作完成。另一方面,如果此长时间运行的操作是异步执行的,则可以释放请求线程,以便它可以在长时间运行的操作仍在运行时做其他事情(例如为其他请求提供服务)。然后,当长时间运行的操作最终完成时,请求线程(或者可能是线程池中的另一个线程)可以从中断的位置开始(因为长时间运行的操作将完成并且其结果现在可用) ),并尽一切可能产生响应。

服务器应用程序示例还解决了有关async-async/await is all about freeing up threads的主要优点的问题的第二部分。

答案 1 :(得分:1)

  

这不是归因,因为它本质上不涉及行动?

通过调用异步方法,您可以在其中执行代码。通常,一种方法将创建一个Task并通过使用return或等待返回它。

开始任务

您可以使用Task.Run(...)启动任务。这样可以安排任务线程池上的一些工作。

等待任务

要获取任务,通常需要调用一些(异步)方法来返回任务。在您async(或使用await)之前,Task.Run()方法的行为类似于常规方法。请注意,如果您等待一系列方法,而“ final”方法仅执行Thread.Sleep()或同步操作-那么您将阻塞初始调用线程,因为没有方法使用过Task的线程池。

您可以通过多种方式执行一些 actual 异步操作:

这些是我想到的,可能还有更多。

通过示例

让我们假设线程ID 1是您从中调用MethodA()的主线程。线程ID 5及更高版本是在其上运行Task的线程(System.Threading.Tasks为此提供了默认的Scheduler)。

public async Task MethodA()
{
    // Thread ID 1, 0s passed total
    var a = MethodB(); // takes 1s
    // Thread ID 1, 1s passed total
    await Task.WhenAll(a); // takes 2s
    // Thread ID 5, 3s passed total

    // When the method returns, the SynchronizationContext
    // can change the Thread - see below
}

public async Task MethodB()
{
    // Thread ID 1, 0s passed total
    Thread.Sleep(1000); // simulate blocking operation for 1s
    // Thread ID 1, 1s passed total

    // the await makes MethodB return a Task to MethodA
    // this task is run on the Task ThreadPool
    await Task.Delay(2000); // simulate async call for 2s
    // Thread ID 2 (Task's pool Thread), 3s passed total
}

我们可以看到MethodAMethodB上被阻止,直到我们遇到一个等待语句。

等待,SynchronizationContext和控制台应用程序

您应该了解任务的一项功能。如果存在一个SynchronizationContext(基本上是非控制台应用程序),它们将确保回调。如果调用的代码不采取措施,则在任务上使用.Result.Wait()时,很容易陷入死锁。参见https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

异步/等待作为语法糖

await基本上只是计划在调用完成后运行以下代码。让我说明一下幕后发生的事情的想法。

这是使用async / await的未转换代码。等待Something方法,因此以下所有代码(Bye)将在Something完成后运行。

public async Task SomethingAsync()
{
    Hello();
    await Something();
    Bye();
}

要对此进行解释,我添加了一个实用程序类Worker,该类只需执行一些操作即可运行,然后在完成时通知。

public class Worker
{
    private Action _action;

    public event DoneHandler Done;
    // skipping defining DoneHandler delegate

    // store the action
    public Worker(Action action) => _action = action;

    public void Run()
    {
        // execute the action
        _action();

        // notify so that following code is run
        Done?.Invoke();
    }
}

现在我们转换后的代码,不再使用异步/等待

public Task SomethingAsync()
{
    Hello(); // this remains untouched

    // create the worker to run the "awaited" method
    var worker = new Worker(() => Something());

    // register the rest of our method
    worker.Done += () => Bye();

    // execute it
    worker.Run();

    // I left out the part where we return something
    // or run the action on a threadpool to keep it simple        
}

答案 2 :(得分:0)

这是简短的答案:

要回答此问题,您只需了解async / await关键字的作用。

我们知道单个线程一次只能做一件事,我们也知道单个线程会在整个应用程序中反弹到各种方法调用和事件ETC。这意味着线程下一步需要去的地方很可能是在幕后某个地方计划或排队的(是,但是我在这里不做解释。)当线程调用某个方法时,该方法会先于其他对象运行完毕可以运行其他方法,这就是为什么最好将长期运行的方法分派给其他线程以防止应用程序冻结。为了将单个方法分成单独的队列,我们​​需要进行一些有趣的编程,或者您可以在方法上放置async签名。这告诉编译器,在某些时候该方法可以分解为其他方法,并放在队列中以便以后运行。

如果这是有道理的,那么您已经在弄清楚await的作用了……await告诉编译器这是该方法将要分解并计划在以后运行的地方。这就是为什么您可以不使用async关键字而使用await关键字的原因;尽管编译器知道这一点并警告您。 await通过使用Task为您完成所有这些工作。

await如何使用Task告诉编译器安排其余方法?当您调用await Task时,编译器会为您调用该Task.GetAwaiter()上的Task方法。 GetAwaiter()返回TaskAwaiterTaskAwaiter实现两个接口ICriticalNotifyCompletion, INotifyCompletion。每个都有一个方法UnsafeOnCompleted(Action continuation)OnCompleted(Action continuation)。然后,编译器将其余方法包装(在await关键字之后),并将其放入Action中,然后调用OnCompletedUnsafeOnCompleted方法并传递{ {1}}作为参数。现在Action完成时,如果成功,它将调用Task,否则,将调用OnCompleted,并在用于启动UnsafeOnCompleted的同一线程上下文中调用它们。它使用Task将线程分派到原始线程。

现在您可以了解,ThreadContextasync都不会执行任何await。他们只是告诉编译器使用一些预先编写的代码为您安排所有代码。事实上;您可以Task未运行的await,它将Task await执行并完成或应用程序结束。

知道这一点;通过手动执行async await所做的事情,让我们变得更hacky并更深入地了解它。


使用异步等待

Task

执行编译器手动执行的操作(某种程度)

注意:尽管此代码有效,但它旨在帮助您从上至下理解异步等待。它不会包含或执行与编译器逐字记录相同的方式。

using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadKey();
        }

        public static async void Test()
        {
            Console.WriteLine($"Before Task");
            await DoWorkAsync();
            Console.WriteLine($"After Task");
        }

        static public Task DoWorkAsync()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
                Task.Delay(1000).Wait();
                Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
            });
        }
    }
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task

课程摘要:

请注意,我的示例using System; using System.Threading.Tasks; namespace Question_Answer_Console_App { class Program { static void Main(string[] args) { Test(); Console.ReadKey(); } public static void Test() { Console.WriteLine($"Before Task"); var task = DoWorkAsync(); var taskAwaiter = task.GetAwaiter(); taskAwaiter.OnCompleted(() => Console.WriteLine($"After Task")); } static public Task DoWorkAsync() { return Task.Run(() => { Console.WriteLine($"{nameof(DoWorkAsync)} starting..."); Task.Delay(1000).Wait(); Console.WriteLine($"{nameof(DoWorkAsync)} ending..."); }); } } } //OUTPUT //Before Task //DoWorkAsync starting... //DoWorkAsync ending... //After Task 中的方法只是一个返回DoWorkAsync()的函数。在我的示例中,Task正在运行,因为在方法中我使用了Task。使用关键字return Task.Run(() =>…不会改变该逻辑。完全一样; await只做我上面提到的事情。

如果您有任何问题,请问,我很乐意回答。

答案 3 :(得分:-6)

开始时,您开始一个任务。这意味着可以通过任何已安装的Multitasaking系统将其拾取并执行。

等待时,您等待一个任务实际上完成,然后再继续。

没有诸如“触发并忘记线程”之类的东西。您总是需要回来,对异常做出反应或对异步操作的结果(数据库查询或WebQuery结果,文件系统操作完成,Dokument发送到最近的打印机池)进行处理。

您可以根据需要启动并在paralell中运行尽可能多的任务。但是迟早您将需要结果才能继续。