使用async await时出现意外结果

3
class Program
{
    static void Main(string[] args)
    {
        var a = new A();

        var result = a.Method();

        Console.Read();
    }
}

class A
{
    public async Task<string> Method()
    {
        await Task.Run(new Action(MethodA));
        await Task.Run(new Action(MethodB));

        return "done";
    }

    public async void MethodA()
    {
        for (var i = 0; i < 10; i++)
        {
            Console.WriteLine("MethodA" + i);
            await Task.Delay(1000);
        }
    }

    public async void MethodB()
    {
        for (var i = 0; i < 10; i++)
        {
            Console.WriteLine("MethodB" + i);
            await Task.Delay(1000);
        }
    }
}

结果输出

enter image description here

我认为这里的期望是先输出MethodA1,MethodA2...MethodA9,然后再开始输出MethodB1...MethodB9。
但似乎不是这样。有人能解释一下吗?谢谢!

编辑:

谢谢回复。实际上我知道如何输出正确的结果,但是我想知道为什么我的代码表现得奇怪,因为我认为它与我的异步/等待的知识相违背。

class A
{
    public async Task<string> Method()
    {
        await Task.Run(new Action(MethodA));

        await Task.Run(async () => {
            for (var i=0;i<10;i++)
                await Task.Delay(100);
            Console.WriteLine("should output before done");
        });

        return "done";
    }

    public async void MethodA()
    {
        var returnString = string.Empty;
        for (var i = 0; i < 10; i++)
        {
            await Task.Delay(200);
        }

        Console.WriteLine("MethodA " + Thread.CurrentThread.ManagedThreadId);

    }
}

我简化了代码,现在它是这样的。第一个await快速将控制权转移到下一个await,然后第二个await表现出我期望的行为,最后输出了"done"。但我不知道这里有什么区别。 结果输出

enter image description here

更新:请记住,示例代码是一种不好的实践。您可以阅读此文章获取更多信息

我的一个朋友认为它们之间的区别在于

await Task.Run(new Action(MethodA));

并不等同于

 await Task.Run(async () => {
            for (var i=0;i<10;i++)
                await Task.Delay(100);
            Console.WriteLine("should output before done");
        });

前者以同步方式调用'methodA',而后者是正确的异步调用。但仍然让我感到困惑,因为如果这是真的,那么异步操作只能由lambda表达式表示。我的意思是,没有像'new async Action(...)'这样的东西,对吧?

更新:

我尝试了Task.Factory.StartNew(Action action),它被认为等效于Task.Run(Action action),然后事情又改变了:

class A
{
    public async Task<string> Method()
    {
        await Task.Factory.StartNew(new Action(MethodA));

        await Task.Factory.StartNew(async () => {
            for (var i=0;i<10;i++)
                await Task.Delay(100);
            Console.WriteLine("should output before done");
        });

        return "done";
    }

    public async void MethodA()
    {
        for (var i = 0; i < 10; i++)
            await Task.Delay(200);
        Console.WriteLine("MethodA");

    }
}

我猜我需要更多的帮助...


2
每当你发现自己在键入“async void”时,而它不是用于事件处理程序时,你可能正在做一些错误的事情。async应该始终返回一个Task - Bradley Uffner
3个回答

7
您的代码表现如此,是因为您启动的任务调用了其中一个方法中的一个 await ,该任务将在方法中的第一个 await 完成后结束,因为它是 async void ,然后继续执行,从而导致 await Task.Run(new Action(MethodA)); 将控制返回到线程,而您的 MethodA 仍在运行。这是因为 async void 基本上是“快速启动并忘记”。

以下是您的代码简化版本中发生的情况。

class A
{
    public async Task<string> Method()
    {
        // You create a task to run MethodA
        await Task.Run(new Action(MethodA));
        // control returns here on the first `await` in MethodA because 
        // it is `async void`. 

        return "done";
    }

    public async void MethodA()
    {
        for (var i = 0; i < 10; i++)
        {
            Console.WriteLine("MethodA" + i);
            // Here the control is returned to the thread and because 
            // it's `async void` an `await` will only block until you reach
            // this point the first time.
            await Task.Delay(1000); // 
        }
    }
}

为了得到想要的结果,您首先需要从您的方法中返回一个Task,以便可以等待它们。然后您就不需要使用Task.Run了。
class A
{
    public async Task<string> Method()
    {
        await MethodA();
        await MethodB();

        return "done";
    }

    public async Task MethodA()
    {
        for (var i = 0; i < 10; i++)
        {
            Console.WriteLine("MethodA" + i);
            await Task.Delay(1000);
        }
    }

    public async Task MethodB()
    {
        for (var i = 0; i < 10; i++)
        {
            Console.WriteLine("MethodB" + i);
            await Task.Delay(1000);
        }
    }
}

谢谢,但实际上我只是想弄清楚为什么我的代码似乎与我的异步/等待知识相悖... - Austin ZHOU
我已经添加了一些解释,说明为什么你的代码不能按照你的期望工作。 - juharr
我仔细阅读了您的评论,并在VS中尝试了它,所有的控制转移都像您的评论所说。最让我困扰的是,在等待关键字之后的同一函数中,我认为进程不会在可等待任务完成之前运行,这就是为什么await MethodA();await MethodB()可以按顺序输出结果的原因。但我不明白为什么await Task.Run()会有如此大的差异。 - Austin ZHOU
如果你让你的方法返回一个 Task,然后执行 await Task.Run(() => await MethodA());,那么它会起作用,因为你等待的任务也在等待 MethodA。基本上,Task.Runawait 和方法之间的另一层,但主要问题是这些方法是 async void - juharr
它继续执行的原因是该函数返回void,调用者没有任务可以知道异步操作何时完成。 - juunas

0

您可能希望从MethodA和MethodB返回Task,而不是在这些方法中写入控制台。像这样:

class Program
{
    static void Main(string[] args)
    {
        var a = new A();

        var result = a.Method();

        Console.Read();
    }
}

class A
{
    public async Task<string> Method()
    {
        Console.WriteLine(await MethodA());
        Console.WriteLine(await MethodB());

        return "done";
    }

    public async Task<string> MethodA()
    {
        var returnString = string.Empty;
        for (var i = 0; i < 10; i++)
        {
            returnString += "MethodA" + i + '\n'+'\r';
            await Task.Delay(100);
        }
        return returnString;
    }

    public async Task<string> MethodB()
    {
        var returnString = string.Empty;
        for (var i = 0; i < 10; i++)
        {
            returnString += "MethodB" + i + '\n'+'\r';
            await Task.Delay(100);
        }
        return returnString;
    }
}

注意:我还没有测试过那段代码,但它应该能够说明一般的思路。

-1

async + await != sync,因为有continuation

更多细节请参考使用 Async 和 Await 进行异步编程 (C# 和 Visual Basic)

Async 方法旨在成为非阻塞操作。

在 async 方法中的 await 表达式不会阻塞当前线程,而是将该表达式余下的方法作为 continuation 注册,并将控制返回给 async 方法的调用者。


1
虽然这些都是技术上正确的,但它并没有以一种易于理解的方式解释代码中的问题;特别是对于刚开始学习异步/等待的人来说。 - Bradley Uffner
@BradleyUffner 嗯..我同意。juharr先生已经给出了相关的代码。我只是试图清晰地阐述这个问题的概念。 - SkyWalker
1
@SkyWalker 谢谢。我仔细阅读了它。在同一页的图片中,您可以看到DoIndependentWork()在await关键字之前,根据我的理解,await关键字将“yield return”给调用线程,并且当可等待任务完成时,它将从await关键字继续执行。所以这就是为什么await MethodA(); await MethodB()可以按预期顺序输出(MethodA1,MethodA2...MethodA9 MethodB1..)的原因。 - Austin ZHOU

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