繁体   English   中英

了解C#中的Async和Await

[英]understanding Async and Await in C#

我正在尝试使用C#学习Async和Await。 我有三个方法,当我尝试调用所有这三个方法时,这使我想知道异步调用实际上如何在C#中工作。

class Content {            

    public async Task<string> Delay1() {
        await Task.Delay(5000);
        return "hello";
    }
    public async Task<string> Delay2() {
        await Task.Delay(5000);
        return "hello";
    }
    public async Task<string> Delay3() {
        await Task.Delay(5000);
        return "hello";
    }
    public async Task Print() {
        Console.WriteLine(await Delay1());
        Console.WriteLine(await Delay2());
        Console.WriteLine(await Delay3());
    }
}

因此,当我调用此方法时:

static void Main() {
    new Content().Print();
    Console.Read();
}

我应该在延迟5秒后同时获取所有这三种方法,但是我的Delay2()方法在Delay1()方法的5秒后被调用,而Delay3()方法在Delay2() 5秒后被调用Delay2()方法。

有人可以帮助我了解C#中Async方法的工作吗?

UPDATE 1 Async和Await假设是异步的,那么为什么我的常规语句被停止执行

public async Task Print()
        {
            Console.WriteLine(await Delay1());

            Console.WriteLine("this is regular flow");
        }

异步不是并发的。 每次等待调用都将阻止执行以下代码,直到任务完成。 在这种情况下,您可以通过将示例更改为如下形式来创建并发

class Program
{
    static void Main()
    {
        new Content().Print().Wait();
        Console.Read();
    }
}

class Content
{

    public async Task<string> Delay1()
    {
        await Task.Delay(5000);
        return "hello";
    }
    public async Task<string> Delay2()
    {
        await Task.Delay(5000);
        return "hello";
    }
    public async Task<string> Delay3()
    {
        await Task.Delay(5000);
        return "hello";
    }

    public async Task Print()
    {
        var tasks = new[] {Delay1(), Delay2(), Delay3()};
        await Task.WhenAll(tasks);
        foreach(var result in tasks.Select(x => x.Result))
        {
            Console.WriteLine(result);
        }
    }
}

您可以彼此独立地启动三个任务,并将它们存储在一个集合中。 然后,您可以调用await Task.WhenAll来阻止执行,直到所有这些任务都完成为止。 之后,您可以遍历结果并根据需要使用它们。

async / await语法允许编写一个比不可读的模式更具可读性的模式。 为了使您理解其真正含义,请考虑将您的代码段重写如下:

class Content
{

    public void Delay1(Action<string> callback)
    {
        //something which takes 5000ms
        callback("hello");
    }
    public void Delay2(Action<string> callback)
    {
        //something which takes 5000ms
        callback("hello");
    }
    public void Delay3(Action<string> callback)
    {
        //something which takes 5000ms
        callback("hello");
    }
    public void Print(Action callback)
    {
        this.Delay1(x=> {
            Console.WriteLine(x);
            this.Delay2(y => {
                Console.WriteLine(y);
                this.Delay3(z=> {
                    Console.WriteLine(z);
                    callback();
                }); 
            });
        });
    }
}

您不应中继系统实际执行的操作(例如,对于Task.Delay)。 它可以将“延迟”中继到其他线程(用于ThreadPool的队列),但是它可以与调用方相同的方式执行作业。

正如@Rob正确指出的那样,异步不是并发的:代码按顺序执行。

只是让您了解而已:仅此而已。

当您调用await时,您正在要求curret方法停止执行,直到“ awaited”调用结束; 所以,当你说:

Console.WriteLine(await Delay1());

您正在要求当前方法(打印)停止,直到Delay1()完成。

为了获得您期望的结果,您必须在不使用await指令的情况下调用它们(使用await调用异步方法不是强制性的)。

这是一个例子:

    public class Content
    {

        public async void Delay1()
        {
            await Task.Delay(5000);
            Console.WriteLine("hello");
        }
        public async void Delay2()
        {
            await Task.Delay(5000);
            Console.WriteLine("hello");
        }
        public async void Delay3()
        {
            await Task.Delay(5000);
            Console.WriteLine("hello");
        }
        public void Print()
        {
            Delay1();
            Delay2();
            Delay3();
        }
    }

编辑:正如@Adrian指出的,Print方法可以比三个Delay方法调用更快地完成。 由于Console.ReadLine();我在测试中没有注意到它;

这是供您查看的摘录。 将其放入控制台应用程序进行测试。

此样本中有两个工作流程,第一个工作流程模仿您的模式(使用助手)。 另一个显示了对whenAll任务使用await以及在初始化类型(在本例中为动态)的分配中使用结果。


使用await的强调概念只是为了确保在继续执行该功能之前,已完成任务。 'Async'关键字确保函数的作用域将在任务中(因此返回任务),并且将有任务执行该作用域(因此需要await )。

如果您不打算在继续之前完成任务,请不要使用async/await

namespace TaskAsyncTests
{
    using System.Threading.Tasks;

    class Program
    {
        static async Task<KeyValuePair<string, long>> TaskThis(Func<string> fn)
        {
            var watch = new System.Diagnostics.Stopwatch();
            watch.Start();
            var task = Task.Run(fn); //fn will be 1sec
            await Task.Delay(1000);  //also being delayed 1sec here
            var result = await task;
            watch.Stop();
            return new KeyValuePair<string, long>(result, watch.ElapsedMilliseconds); //result should only be approx. 1 sec though
        }

        static void Main(string[] args)
        {
            var watch = new System.Diagnostics.Stopwatch();
            watch.Start();
            var results = Run(Task.WhenAll(new[]
            {
                TaskThis(LongProcessingFunction),
                TaskThis(LongProcessingFunction),
                TaskThis(LongProcessingFunction),
            }));
            watch.Stop();
            foreach (KeyValuePair<string, long> item in results)
            {
                Console.WriteLine(@"result:= '{0}' ElapsedMilliseconds := {1}", item.Key, item.Value.ToString());
            }
            Console.WriteLine("total ElapsedMilliseconds := {0}", watch.ElapsedMilliseconds);

            watch.Reset();
            watch.Start();
            var result = Run(GetSomethingAsync());
            watch.Stop();

            Console.WriteLine(@"result->PropertyOne := '{0}' ElapsedMilliseconds := {1}", result.PropertyOne.Key, result.PropertyOne.Value.ToString());
            Console.WriteLine(@"result->PropertyTwo := '{0}' ElapsedMilliseconds := {1}", result.PropertyTwo.Key, result.PropertyTwo.Value.ToString());
            Console.WriteLine(@"result->PropertyThree := '{0}' ElapsedMilliseconds := {1}", result.PropertyThree.Key, result.PropertyThree.Value.ToString());
            Console.WriteLine("total ElapsedMilliseconds := {0}", watch.ElapsedMilliseconds);

            Console.ReadLine();
        }

        static string LongProcessingFunction()
        {
            Task.Delay(1000).Wait();

            return "Hello World";
        }

        static T Run<T>(Task<T> taskRunner)
        {
            return taskRunner.Result;
        }

        static T[] Run<T>(Task<T[]> taskRunner)
        {
            return taskRunner.Result;
        }

        static async Task<dynamic> GetSomethingAsync()
        {
            var resultsTask = Task.WhenAll(new[] 
            {
                TaskThis(LongProcessingFunction),
                TaskThis(LongProcessingFunction),
                TaskThis(LongProcessingFunction)
            }).ConfigureAwait(false);

            // do other stuff here
            Task.Delay(2000).Wait();

            var results = await resultsTask;
            return new
            {
                PropertyOne = results[0],
                PropertyTwo = results[1],
                PropertyThree = results[2]
            };
        }
    }
}

我的结果是:

result:= 'Hello World' ElapsedMilliseconds := 1025
result:= 'Hello World' ElapsedMilliseconds := 1014
result:= 'Hello World' ElapsedMilliseconds := 1014
total ElapsedMilliseconds := 1028
result->PropertyOne := 'Hello World' ElapsedMilliseconds := 1001
result->PropertyTwo := 'Hello World' ElapsedMilliseconds := 1001
result->PropertyThree := 'Hello World' ElapsedMilliseconds := 1000
total ElapsedMilliseconds := 2001

用引文更新

由于我的答案因我不知道的原因而被否决,因为除罗伯之外,拒绝投票的人没有给出任何理由要求除罗斯外,我在这里为我的答案辩护。

这里的目的不是要证明任何人是错误的,而是要确保OP接收到正确的信息和事实。

那么,OP的问题是什么? 我认为有两个问题:

  1. 有人可以帮助我了解C#中Async方法的工作吗? (明确要求)
  2. 为什么我的代码没有按照我认为的方式做?

发布了许多答案来修复代码。 纳尔逊的答案也具有正确的代码,因此我从未投票过。 我的问题不是这里提供的解决方案,而是“异步不是并发”的说法。 我还想用简单的英文解释异步等待的工作原理,因为OP明确要求这样做。 所以这就是为什么我仍然坚持自己的立场的原因:

一位受人尊敬的作者,SO成员Stephen Cleary在其关于并发的书中指出了这一点:

异步编程

一种使用期货或回调来避免不必要的线程的并发形式。

未来(或承诺)是表示将来将要完成的某些操作的类型。 .NET中现代的未来类型是“任务”和“任务”。 较早的异步API使用回调或事件而不是期货。 异步编程以异步操作的思想为中心:启动的某些操作将在一段时间后完成。 在进行操作时,它不会阻塞原始线程; 启动操作的线程可以自由执行其他工作。 操作完成后,它会通知其将来或调用其完成回调事件,以使应用程序知道操作已完成。

他进一步说:

异步编程是一种强大的并发形式,但是直到最近,它仍需要极其复杂的代码。 VS2012中的async和await支持使异步编程几乎与同步(非并发)编程一样容易。

是一个stackoverflow答案,其中同一作者回答了另一个类似的问题。

是本书的链接,而我引用的部分来自1.1。 并发简介。

现在应该阐明并支持我关于异步并发的声明。

我不确定我的“示例与并发无关”,但Rob可以对此进行解释。

我不确定答案中还有哪些其他误导性内容 ,所以我无法为自己辩护。 我不认为我有任何误导性陈述,但如果有的话,我,OP和其他成员都希望了解他们,以便它可以使所有人受益。

引用结束更新

这是不正确的,因为其他一些回答表明异步不是并发形式 异步编程IS(是的)是一种并发形式。 但是,话虽如此,这并不意味着这项工作将需要更少的时间。 这项工作仍将花费相同的时间(由于上下文切换,可能会更长)。 但是诀窍在于,将在不等待先前工作完成的情况下同时完成工作(先延迟1然后再延迟2然后再延迟3)。 让我用您的代码解释所有这些。

这是您在Print方法中的代码中所做的事情:让我们调用执行Print P1的线程

  1. 进行Delay1 AND并在返回时转到下面的2。 在Delay1方法中,另一个线程被赋予任务休眠。 让我们称之为T1。 T1请睡眠5秒钟,然后返回“ hello”。
  2. 进行Delay2 AND并在返回时转到下面的3。 在Delay2方法中,另一个线程被赋予任务休眠。 让我们称之为T2。 T2请睡眠5秒钟,然后返回“ hello”。
  3. 进行Delay3 AND并在返回时转到下一行(没有下一行,因此程序将完成)。 在Delay3方法中,另一个线程被赋予睡眠任务。 让我们称之为T3。 T3请睡眠5秒钟,然后返回“ hello”。

因此,您的打印方法将调用Delay1并等待其返回。 然后,它调用Delay2并等待它返回。 然后3并等待它返回。 所有这些都将在睡眠5秒后返回(每个)。

因此,您可能会问:“那么这一切的意义何在?”?

关键是在您的打印方法中,您可以同时调用所有它们:执行Delay1,Delay2和Delay3,当它们全部完成后,转到下一行。

那么,那能给您带来什么呢?

好吧,如果您在Delay方法中调用某些Web服务,并且每个Web服务都需要10秒才能返回,那么您将在几乎同一时间全部调用它们,因此这三个都将开始工作。 他们可能会在10秒多一点后全部返回。 因此,您基本上将它们全部同时发送给他们,因此“ 并发 ”。 如果不异步调用它们,则Delay1需要完成(10秒),然后完成Delay2(10秒),然后完成Delay3(10秒),因此总共需要30秒。

像这样思考:

您是麦当劳的经理。 您有很多员工(线程池),需要他们来工作(Delay1,Delay2和Delay3)。 最好是先让一名员工先执行Delay1,然后再执行Delay2,然后再执行Delay3,或者分配3个不同的员工(线程池中的线程),然后让1名员工执行Delay1,然后告诉第二名员工执行Delay2,而不要等待Delay1完成并询问第三名员工要做Delay3。 显然,您会选择3名员工。

现在在线程池的世界中有所不同。 可能为2个任务选择了相同的线程,因为一个线程完成了该工作,然后回到池中,然后又被赋予了另一个任务。 在您的示例中,这种情况极不可能发生,因为他们需要睡眠5秒钟。 只有一种方法可以做到这一点。

请注意,在某些情况下,一项任务取决于另一项任务,您别无选择,只能等待。 例如,如果Delay1正在从远程计算机上获取文件,而Delay2正在获取另一个文件,并且Delay3将同时处理这两个文件,则Delay1和Delay2可以“同时执行”,但是Delay3需要等待它们返回才能开始。

为避免混淆,请注意,在您的情况下,T1,T2和T3仅在睡眠。 您好的返回将由P1线程完成。 有一些方法可以使P1不必这样做,但是在您的情况下,一旦完成睡眠,控制权将交给P1。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM