关于c#中的异步和等待(Async and Await)是如何工作的

4

我在这个网站上看到一些关于Async和Await使用的帖子。有些人说Async和Await会在单独的后台线程上完成其工作,也就是生成一个新的后台线程,而有些人则表示不会,也就是Async和Await不会启动任何单独的后台线程来完成其工作。

所以,有没有人可以告诉我,在使用Async和Await时会发生什么。

这里是一个小程序

class Program
{
    static void Main(string[] args)
    {
        TestAsyncAwaitMethods();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }

    public async static void TestAsyncAwaitMethods()
    {
        await LongRunningMethod();
    }

    public static async Task<int> LongRunningMethod()
    {
        Console.WriteLine("Starting Long Running method...");
        await Task.Delay(5000);
        Console.WriteLine("End Long Running method...");
        return 1;
    }
}

输出结果为:

Starting Long Running method...
Press any key to exit...
End Long Running method...

@Mou:你可以使用isbackground属性来检查它是后台线程还是UI线程。 - 107
你认为它会启动一个后台线程吗? - Mou
这篇文章将回答你所有的问题。仔细阅读它。简而言之,async-await不使用任何线程。它取决于方法的实现是否使用线程。此外,这个链接也可以帮助你理解:http://stackoverflow.com/a/25078752/2530848 - Sriram Sakthivel
自己找出来,使用调试器。使用断点和调试>窗口>线程调试器窗口。你迟早会需要它,最好现在就熟悉一下。 - Hans Passant
@Mou:你可能会发现我的没有线程博客文章很有用。 - Stephen Cleary
显示剩余2条评论
7个回答

7
问题在于async/await关注的是异步性,而不是线程。
如果使用Task.Run,它会确实使用后台线程(通过线程池,通过任务并行库)。
但是,对于IO操作,它依赖于IO完成端口来通知何时完成操作。 async/await所做的唯一保证是,当操作完成时,它将返回到调用方所在的同步上下文中。在实际应用中,这意味着它将返回到UI线程(在Windows应用程序中)或可以返回HTTP响应的线程(在ASP.NET中)。

你是在说Task.Run会生成新的后台线程,但async/await不会启动任何线程……我理解得对吗? - Mou
1
我相信可能会有例外,但这是一个合理的基本假设。 - Richard Szalay
请列出使用async/await的优势,因为在异步编程中还有其他方法,如bgworker、task.run、threadpool和thread类。在什么样的场景下人们会使用async/await?如果时间允许,请回答。谢谢。 - Mou
这已经接近于一次对话了,而这是堆栈溢出的禁忌。bgworker和task.run都使用线程池(它使用线程)。async/await与bgworker共享异步性,但提供更熟悉的编程体验。 - Richard Szalay
你说过"async/await与bgworker共享异步性",但是background worker是不同的类...那么async/await与background worker类有什么关系呢? - Mou
显示剩余2条评论

6

为了更好地理解编译器的内部运行机制,可以使用SharpLab,如果您将简短的示例粘贴进去,您将了解到C#编译器是如何重写包含async/await代码的过程:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading.Tasks;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
internal class Program
{
    [CompilerGenerated]
    private sealed class <TestAsyncAwaitMethods>d__1 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncVoidMethodBuilder <>t__builder;

        private TaskAwaiter<int> <>u__1;

        private void MoveNext()
        {
            int num = <>1__state;
            try
            {
                TaskAwaiter<int> awaiter;
                if (num != 0)
                {
                    awaiter = LongRunningMethod().GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        num = (<>1__state = 0);
                        <>u__1 = awaiter;
                        <TestAsyncAwaitMethods>d__1 stateMachine = this;
                        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    awaiter = <>u__1;
                    <>u__1 = default(TaskAwaiter<int>);
                    num = (<>1__state = -1);
                }
                awaiter.GetResult();
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult();
        }

        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }

    [CompilerGenerated]
    private sealed class <LongRunningMethod>d__2 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder<int> <>t__builder;

        private TaskAwaiter <>u__1;

        private void MoveNext()
        {
            int num = <>1__state;
            int result;
            try
            {
                TaskAwaiter awaiter;
                if (num != 0)
                {
                    Console.WriteLine("Starting Long Running method...");
                    awaiter = Task.Delay(5000).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        num = (<>1__state = 0);
                        <>u__1 = awaiter;
                        <LongRunningMethod>d__2 stateMachine = this;
                        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    awaiter = <>u__1;
                    <>u__1 = default(TaskAwaiter);
                    num = (<>1__state = -1);
                }
                awaiter.GetResult();
                Console.WriteLine("End Long Running method...");
                result = 1;
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult(result);
        }

        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }

    private static void Main(string[] args)
    {
        TestAsyncAwaitMethods();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }

    [AsyncStateMachine(typeof(<TestAsyncAwaitMethods>d__1))]
    [DebuggerStepThrough]
    public static void TestAsyncAwaitMethods()
    {
        <TestAsyncAwaitMethods>d__1 stateMachine = new <TestAsyncAwaitMethods>d__1();
        stateMachine.<>t__builder = AsyncVoidMethodBuilder.Create();
        stateMachine.<>1__state = -1;
        AsyncVoidMethodBuilder <>t__builder = stateMachine.<>t__builder;
        <>t__builder.Start(ref stateMachine);
    }

    [AsyncStateMachine(typeof(<LongRunningMethod>d__2))]
    [DebuggerStepThrough]
    public static Task<int> LongRunningMethod()
    {
        <LongRunningMethod>d__2 stateMachine = new <LongRunningMethod>d__2();
        stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
        stateMachine.<>1__state = -1;
        AsyncTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
        <>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
}

正如在SO上其他答案中指出的那样(例如这个),async / await将代码重写为状态机,就像使用返回IEnumeratorIEnumerableIEnumerator<T>IEnumerable<T>的方法一样。不同之处在于,对于async方法,你可以返回任何一个

关于最后一个弹药,您可以在这里这里那里阅读更多关于它(即它是基于模式的)的信息。这还涉及其他微妙的选择,超出了您问题的范围,但您可以在这里这里有关ValueTask<TResult>IValueTaskSource<TResult>等的简短说明。 代码重写的行为被委托给编译器,Roslyn基本上使用AsyncRewriter类来了解如何重写不同的执行路径和分支以获得等效的代码。
在包含yieldasync关键字的有效代码中,无论哪种情况,都有一个初始状态,并且根据分支和执行路径,背后发生的MoveNext()调用将从一个状态转移到另一个状态。
在了解到有效的async代码的情况下,下面这种片段:
case -1:
    HelperMethods.Before();
    this.awaiter = AsyncMethods.MethodAsync(this.Arg0, this.Arg1).GetAwaiter();
    if (!this.awaiter.IsCompleted)
    {
        this.State = 0;
        this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
    }
    break;

可以大致翻译为(详见Dixin的博客):

case -1: // -1 is begin.
    HelperMethods.Before(); // Code before 1st await.
    this.currentTaskToAwait = AsyncMethods.MethodAsync(this.Arg0, this.Arg1); // 1st task to await
    // When this.currentTaskToAwait is done, run this.MoveNext() and go to case 0.
    this.State = 0;
    this.currentTaskToAwait.ContinueWith(_ => that.MoveNext()); // Callback
    break;

请记住,如果您将void作为async方法的返回类型,则不会有太多currentTaskToAwait =]。

有些人说Async和Await在单独的后台线程上完成其工作,这意味着会产生一个新的后台线程,而有些人则认为Async和Await不会启动任何单独的后台线程来完成其工作。

关于您的代码,您可以跟踪使用哪个线程(即ID)以及它是否来自池:

public static class Program
{
    private static void DisplayCurrentThread(string prefix)
    {
        Console.WriteLine($"{prefix} - Thread Id: {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"{prefix} - ThreadPool: {Thread.CurrentThread.IsThreadPoolThread}");
    }

    public static void Main(params string[] args)
    {
        DisplayCurrentThread("Main Pre");

        TestAsyncAwaitMethods();

        DisplayCurrentThread("Main Post");

        Console.ReadLine();
    }

    private static async void TestAsyncAwaitMethods()
    {
        DisplayCurrentThread("TestAsyncAwaitMethods Pre");

        await LongRunningMethod();

        DisplayCurrentThread("TestAsyncAwaitMethods Post");
    }

    private static async Task<int> LongRunningMethod()
    {
        DisplayCurrentThread("LongRunningMethod Pre");
        Console.WriteLine("Starting Long Running method...");

        await Task.Delay(500);

        Console.WriteLine("End Long Running method...");
        DisplayCurrentThread("LongRunningMethod Post");

        return 1;
    }
}

将输出例如:

Main Pre - Thread Id: 1
Main Pre - ThreadPool: False
TestAsyncAwaitMethods Pre - Thread Id: 1
TestAsyncAwaitMethods Pre - ThreadPool: False
LongRunningMethod Pre - Thread Id: 1
LongRunningMethod Pre - ThreadPool: False
Starting Long Running method...
Main Post - Thread Id: 1
Main Post - ThreadPool: False
End Long Running method...
LongRunningMethod Post - Thread Id: 4
LongRunningMethod Post - ThreadPool: True
TestAsyncAwaitMethods Post - Thread Id: 4
TestAsyncAwaitMethods Post - ThreadPool: True

你可以注意到,LongRunningMethod 方法在 Main 方法之后终止,这是因为你将异步方法的返回类型设置为 void。只有事件处理程序才应该使用 async void 方法(请参见 Async/Await - Best Practices in Asynchronous Programming)。
另外,正如 i3arnon 已经提到的那样,由于没有传递上下文,因此程序确实会从线程池中重用线程以在异步方法调用后恢复执行。
关于这些“上下文”,我建议你阅读 那篇文章,这篇文章将澄清什么是上下文,特别是 SynchronizationContext
注意,我说的是线程池线程“恢复”,而不是执行异步代码块,您可以在这里找到更多信息。
异步方法通常旨在利用底层调用固有的任何延迟,通常是IO,例如写入、读取磁盘上的某些内容、查询网络上的某些内容等等。
真正异步方法的目的是避免使用线程进行IO操作,这可以帮助应用程序在有更多请求时扩展。通常可以使用async资源来处理更多的ASP.NET WebAPI请求,因为每个请求的线程在它们击中数据库或其他您正在进行的async调用时将被“释放”。
我建议您阅读此问题的答案。
无返回值的异步方法有一个特定的用途:使异步事件处理程序成为可能。虽然可以有返回某种实际类型的事件处理程序,但这与语言不兼容;调用返回类型的事件处理程序非常笨拙,并且事件处理程序实际上返回某些东西的概念并不太合理。
事件处理程序自然返回void,因此异步方法返回void,以便您可以拥有异步事件处理程序。但是,异步void方法的一些语义与异步Task或异步Task方法的语义略有不同。
避免这种情况的一种方法是利用C# 7.1 feature 并期望将void作为返回类型改为Task
public static class Program
{
    private static void DisplayCurrentThread(string prefix)
    {
        Console.WriteLine($"{prefix} - Thread Id: {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"{prefix} - ThreadPool: {Thread.CurrentThread.IsThreadPoolThread}");
    }

    public static async Task Main(params string[] args)
    {
        DisplayCurrentThread("Main Pre");

        await TestAsyncAwaitMethods();

        DisplayCurrentThread("Main Post");

        Console.ReadLine();
    }

    private static async Task TestAsyncAwaitMethods()
    {
        DisplayCurrentThread("TestAsyncAwaitMethods Pre");

        await LongRunningMethod();

        DisplayCurrentThread("TestAsyncAwaitMethods Post");
    }

    private static async Task<int> LongRunningMethod()
    {
        DisplayCurrentThread("LongRunningMethod Pre");
        Console.WriteLine("Starting Long Running method...");

        await Task.Delay(500);

        Console.WriteLine("End Long Running method...");
        DisplayCurrentThread("LongRunningMethod Post");

        return 1;
    }
}

然后你会得到

Main Pre - Thread Id: 1
Main Pre - ThreadPool: False
TestAsyncAwaitMethods Pre - Thread Id: 1
TestAsyncAwaitMethods Pre - ThreadPool: False
LongRunningMethod Pre - Thread Id: 1
LongRunningMethod Pre - ThreadPool: False
Starting Long Running method...
End Long Running method...
LongRunningMethod Post - Thread Id: 4
LongRunningMethod Post - ThreadPool: True
TestAsyncAwaitMethods Post - Thread Id: 4
TestAsyncAwaitMethods Post - ThreadPool: True
Main Post - Thread Id: 4
Main Post - ThreadPool: True

这更符合您通常的期望。

关于 async / await 的更多资源:


1
是的,阿姨,没错,这就是我要找的。谢谢。 - Soner from The Ottoman Empire

3
你的两个陈述可能都是正确的,但有些令人困惑。
Async-await通常在单独的后台线程上完成,但这并不意味着它会启动任何单独的后台线程来完成工作。
这些异步操作的重点是在执行异步操作时不占用线程,因为真正的异步操作不需要线程。
在该操作之前的部分可以是CPU绑定的,并且需要一个线程来执行,并由调用线程执行。在该操作之后的部分(通常称为完成)也需要一个线程。如果有SynchronizationContext(例如在UI或asp.net应用程序中),或者TaskScheduler,则由它们处理该部分。如果没有,则将该部分安排在ThreadPool上,由已经存在的后台线程执行。
因此,在您的示例中,Task.Delay创建一个在5秒后完成的Task。在延迟期间不需要线程,因此可以使用async-await。
您的示例的流程如下:主线程开始执行Main,调用TestAsyncAwaitMethods,调用LongRunningMethod,打印第一条消息,调用Task.Delay,将方法的其余部分注册为在Task.Delay完成后执行的继续项,返回到Main,打印消息并同步等待(阻塞)Console.ReadLine。
5秒钟后,Task.Delay中的计时器结束并完成从Task.Delay返回的Task。然后在ThreadPool上调度继续项(因为这是控制台应用程序),并且被分配该任务的ThreadPool线程打印“End Long Running method...”。
总之,真正的异步操作不需要线程才能运行,但它在完成后确实需要一个线程,通常是来自ThreadPool的后台线程,但不一定如此。

谢谢回复。您说 “Async-await 通常在单独的后台线程上完成,但这并不意味着它会启动任何单独的后台线程来完成作业”,在这里您说 Async-await 在单独的线程上完成其工作,但它并没有创建或启动任何线程,那么异步等待在哪个后台线程上工作或完成其工作,则会产生一些困惑。 - Mou
@Mou ThreadPool。除非有一个SynchronizationContext或者TaskScheduler在异步操作完成时采取其他方式,否则其继续执行将被安排在ThreadPool上。因此,一个后台线程被借用并返回,但不是用于实际操作。 - i3arnon
抱歉,我不清楚你想表达什么。能否请您详细说明一下? - Mou
@Mou 一个真正的异步操作,比如 Task.Delay 中的延迟,不需要线程来运行,但是如果你有一个带有 await Task.Delay(1000); 的异步方法,并且在此之后还有代码,那么这段代码需要一些线程来继续运行。除非另有规定,否则该线程将是一个 ThreadPool 线程。这就是为什么说异步等待不会创建后台线程,并且说异步等待在后台线程上完成是正确的。 - i3arnon

1

你问错了问题

实际上,你在问,包裹是通过船还是飞机送到我家门口的?

关键是你的门口并不关心包裹是通过海运还是空运送达的。

然而,微软开发Task/async/await框架的主要原因是利用基于事件的编程而不是基于线程的编程。

通常情况下,基于事件的编程比基于线程的编程更加高效和快速。这就是为什么大多数.net API使用它的原因。然而,直到现在,大多数人都避免使用基于事件的编程,因为它非常难以理解(再次强调,async/wait的出现是为了使其变得简单)。


你为什么认为async/wait与基于事件的编程有关?async/wait关联哪个事件?我知道bgWorker是基于事件的编程,但请进一步解释以证明async/wait也是基于事件的编程。 - Mou
@Mou 我的意思不是指EAP模式编程。我是指异步是通过回调和中断实现的。 - Aron
你能否提供回调函数和中断的示例代码? - Mou

0
需要理解两件事情:a) async/await使用任务(任务使用线程池)b) async/await不是用于并行工作。
只需编译此代码并查看ID即可:
static void Main(string[] args)
    {
        Console.WriteLine("Id main thread is: {0}", Thread.CurrentThread.ManagedThreadId);
        TestAsyncAwaitMethods();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }

    public async static void TestAsyncAwaitMethods()
    {
        Console.WriteLine("Id thread (void - 0) is: {0}", Thread.CurrentThread.ManagedThreadId);
        var _value = await LongRunningMethod();
        Console.WriteLine("Id thread (void - 1) is: {0}", Thread.CurrentThread.ManagedThreadId);
    }

    public static async Task<int> LongRunningMethod()
    {
        Console.WriteLine("Id thread (int) is: {0}", Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine("Starting Long Running method...");
        await Task.Delay(1000);
        Console.WriteLine("End Long Running method...");
        return 1;
    }

0

只有在标记为async的方法内部才能调用await。一旦你await一个函数,框架就知道如何记住你当前的调用环境,并在等待的函数完成后将控制权返回给它。

你只能await返回Tasks的函数。所以,所有的await都处理的是返回的Task对象(在任务返回之前,你正在等待的方法是同步执行的)。

为了提供给你一个Task,你正在等待的方法可以创建一个新的线程来完成它的工作,也可以同步返回一个带有值的已完成任务(从结果创建任务),它可以做任何它想做的事情。await所做的只是将控制权交还给你的函数的父级,直到你从可等待的方法中收到的Task对象完成为止。在那时,它将从await行继续执行你的方法。


0

最简单的解决方案是:

await LongRunningMethod().wait();

这会导致主线程等待(非阻塞),直到LongRunningMethod执行完成。


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