C#中的异步等待和任务

3

我正在测试异步等待和任务功能,但似乎我漏掉了一些东西。

当我像这样写:A是起点

void A()
{
   Debug.WriteLine("pre B");
   B();
   Debug.WriteLine("post B");
}

async void B()
{
    Debug.WriteLine("pre C");
    await C();
    Debug.WriteLine("post C")
}

async Task C()
{
   await Task.Yield();
   Debug.WriteLine("pre D");
   await D();
   Debug.WriteLine("post D");
}

async Task D()
{
   Debug.WriteLine("inside D");
}

我在调试控制台中得到的输出为:
pre B,
pre C,
pre D,
inside D,
post D,
post C,
post B

它是否以异步方式运行?我猜不是,但它应该以什么样的方式运行呢?
pre B,
pre C,
post B,
post C,
pre D,
inside D,
post D

所以问题出在哪里呢?

2
仅仅因为它是异步运行的,并不意味着顺序会按照你的期望进行。尝试在你的函数中加入一些延时,以确保它们不会太快地完成。 - brettwhiteman
唯一可能移动的是 post B 输出。请自行阅读 B 方法的主体(C 同理)- 写下 pre C,然后等待 C 完成,再写下 post C。从逻辑上讲,只有在 C 完成后才能写入 post C。此时,所有与 D 相关的消息必须已经被写入。 - Damien_The_Unbeliever
它们在同一线程上运行,所以不行。 - Oguz Ozgul
2
我猜你对async/await有一些误解,通常通过阅读Eric Lippert的《C# 5.0中的异步编程第二部分:等待何时到来?》可以解决 - 例如,“异步方法的整个重点在于尽可能地保持在当前线程上”。 - Damien_The_Unbeliever
我修改了代码,在每个Debug.WriteLine()之前和之后添加了Thead.Sleep(2000),但是顺序仍然相同。正如您的链接所说:“如果我们等待的任务尚未完成,则将此方法的其余部分注册为该任务的继续,并立即返回给调用者;任务在完成时将调用继续。”为什么这里没有发生呢? - Dinkar Thakur
显示剩余3条评论
4个回答

3
我建议您阅读我的async简介,然后再查看我的async最佳实践文章。特别是其中一个最佳实践是仅在事件处理程序中使用async voidasync的整个意义在于启用异步代码,同时使该代码以类似于同步代码的方式编写。因此,如果外部方法await了内部方法返回的任务,则外部方法将不会继续执行,直到内部方法完成为止。
例如,同步代码可能如下所示:
void A()
{
  Debug.WriteLine("pre B");
  B();
  Debug.WriteLine("post B");
}

void B()
{
  Debug.WriteLine("inside B");
  Thread.Sleep(1000);
  Debug.WriteLine("still inside B");
}

相应的异步代码如下所示:
async Task A()
{
  Debug.WriteLine("pre B");
  await B();
  Debug.WriteLine("post B");
}

async Task B()
{
  Debug.WriteLine("inside B");
  await Task.Delay(1000);
  Debug.WriteLine("still inside B");
}

虽然代码现在将异步执行,但输出与同步版本完全相同。
如果您想要一个方法来开始并发操作,然后继续执行,您只需调用方法,然后稍后使用await即可:
async Task A()
{
  // Start both tasks
  Debug.WriteLine("pre B1");
  Task b1 = B();
  Debug.WriteLine("post B1, pre B2");
  Task b2 = B();
  Debug.WriteLine("post B2, pre await");

  // (asynchronously) wait for them to complete
  await Task.WhenAll(b1, b2);
  Debug.WriteLine("post await");
}

async Task B()
{
  Debug.WriteLine("inside B");
  await Task.Delay(1000);
  Debug.WriteLine("still inside B");
}

在这种情况下,您将看到B的两个执行都开始了,并且在两个B完成后,A将继续执行。

我正在回答,因为我的测试结果与您提出的不同。必须编写代码,因此将其发布为答案。 - Dinkar Thakur

1

我已经更改了代码,现在它看起来像这样:

using System;
using System.Threading.Tasks;

namespace TaskClass
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Pre A");
            A();
            Console.WriteLine("Post A");
            Console.ReadKey();
        }
        static async Task A()
        {
            Console.WriteLine("pre B");
            await B();
            Console.WriteLine("post B");
        }

        static async Task B()
        {
            Console.WriteLine("inside B");
            await Task.Delay(10000);
            Console.WriteLine("still inside B");
        }

    }
}

我得到的输出结果如预期。
前A
前B
内部B
后A
仍然在B内部
后B
所以你看,当我碰到 await Task.Delay(10000); 时,控制权就回到了 Main 方法,这可以从它在打印 Post A 之前打印 still inside B 并且在 Console.WriteLine("Post A"); 之后也是如此。它只等待 Console.ReadKey(); 是因为一旦我按下任何键,执行就会停止。它不会等待 Console.WriteLine("still inside B");

如此简单又如此奇怪。.net!! :)


0

这是正常的。Async/Await 意味着该方法被暂停,直到任务完成。该任务异步运行,但当前线程等待它,且当前线程不会被阻塞。

如果您不想在方法等待可能需要很长时间才能返回结果时阻塞 UI 线程,则此功能特别有用。

来自 MSDN

异步提高响应能力
异步对于那些可能会阻塞的活动是必不可少的,比如当你的应用程序访问网络时。访问网络资源有时会很慢或延迟。如果这样的活动在同步进程中被阻塞,整个应用程序就必须等待。在异步进程中,应用程序可以继续进行其他不依赖于网络资源的工作,直到可能会阻塞的任务完成。
下表显示了异步编程改善响应能力的典型领域。列出的来自.NET Framework 4.5和Windows Runtime的API包含支持异步编程的方法。
应用程序领域 支持包含异步方法的API
Web访问 HttpClient,SyndicationClient
文件操作 StorageFile,StreamWriter,StreamReader,XmlReader 图像处理 MediaCapture,BitmapEncoder,BitmapDecoder WCF编程 同步和异步操作
异步对于访问UI线程的应用程序尤其有价值,因为所有与UI相关的活动通常共享一个线程。如果同步应用程序中有任何进程被阻塞,所有进程都会被阻塞。您的应用程序停止响应,您可能会得出它已经失败的结论,而实际上它只是在等待。
例如,您可以拥有一个返回值的方法。
public async Task<int> LongComputation()

然后,在你的代码中有:

var result = await LongComputation();

当前线程未被阻塞。当结果可用时,您可以在变量中获取它,并在当前线程中继续使用。


如果你想异步地启动一个新任务,可以这样做。
Task.Factory.StartNew(async () =>
                {
                    Debug.WriteLine("async start");
                    await Task.Delay(5000);
                    Debug.WriteLine("async end");
                });

在你的B方法中,你可以尝试以下操作

async void B()
{
    Debug.WriteLine("pre C");
    var taskC = C();
    Debug.WriteLine("post C")
    await taskC;
}

那么我该如何检查当前线程未被阻塞?根据我的例子,它似乎是一个接一个地运行,函数A()正在等待函数D()完成。 - Dinkar Thakur
1
同时,Task.Factory.StartNew或Task.Run(()=>)将创建一个全新的线程,这是完全不同的故事。不是吗? - Dinkar Thakur
在您提供的第一个链接中,您可以看到任务getStringTask被启动,但稍后会被awaitedgetStringTask在另一个线程中启动。当前线程继续执行并稍后await它。 - Daniel
它并不等待控制返回到被调用的方法。当任务完成时,其结果会返回给等待它的方法。正如第二个链接所给出的那样。 - Dinkar Thakur
让我们在聊天中继续这个讨论 - Dinkar Thakur
显示剩余4条评论

0

我猜它是同步运行的,因为你使用了await语句,它会等待你的方法完成。 因此,如果您想获得类似于2d变量的结果,请尽量不要使用await。


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