异步/等待问题-等待的操作结束了怎么办?

时间:2019-08-10 09:04:28

标签: c# .net async-await

我想我已经阅读了大约20篇关于异步/等待的文章,关于SO的多个问题,但在理解其工作方式方面仍然存在很多差距,尤其是涉及多个线程以及何时全部使用一个线程完成的时候。

我为这段代码编写了自己,以测试一种情况:

static async Task Main(string[] args)
        {
            var longTask = DoSomethingLong();

            for (int i = 0; i < 1000; i++)
                Console.Write(".");

            await longTask;
        }

        static async Task DoSomethingLong()
        {
            await Task.Delay(10);
            for (int i = 0; i < 1000; i++)
                Console.Write("<");
        }

我明确地让Main继续将点写入控制台,同时在DoSomethingLong内部延迟执行被阻止。我看到延迟结束后,点写入和<符号写入开始相互干扰。

我只是想不通:是两个线程同时进行这项工作吗?一个写点,另一个写其他字符?还是以某种方式在这些上下文之间切换?我会说这是第一选择,但我仍然不确定。使用异步/等待时,何时可以期望创建其他线程?我仍然不清楚。

也总是执行Mai,到达等待状态然后再回写点的相同线程吗?

2 个答案:

答案 0 :(得分:2)

var longTask = DoSomethingLong();

此行创建一个Task。与Task.Run()创建的任务相比,这是一个不同的任务。这是基于状态机的任务,由C#编译器自动生成。这是编译器生成的内容(从sharplab.io复制粘贴):

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]
public class Program
{
    [CompilerGenerated]
    private sealed class <DoSomethingLong>d__0 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder <>t__builder;

        private int <i>5__1;

        private TaskAwaiter <>u__1;

        private void MoveNext()
        {
            int num = <>1__state;
            try
            {
                TaskAwaiter awaiter;
                if (num != 0)
                {
                    awaiter = Task.Delay(10).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        num = (<>1__state = 0);
                        <>u__1 = awaiter;
                        <DoSomethingLong>d__0 stateMachine = this;
                        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    awaiter = <>u__1;
                    <>u__1 = default(TaskAwaiter);
                    num = (<>1__state = -1);
                }
                awaiter.GetResult();
                <i>5__1 = 0;
                while (<i>5__1 < 1000)
                {
                    Console.Write("<");
                    <i>5__1++;
                }
            }
            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);
        }
    }

    [AsyncStateMachine(typeof(<DoSomethingLong>d__0))]
    [DebuggerStepThrough]
    private static Task DoSomethingLong()
    {
        <DoSomethingLong>d__0 stateMachine = new <DoSomethingLong>d__0();
        stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
        stateMachine.<>1__state = -1;
        AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
        <>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
}

实际上,发生的情况是DoSomethingLong方法的第一部分是同步执行的。它是第一个await之前的部分。 在这种特定情况下,第一个await前没有任何内容,因此对DoSomethingLong的调用几乎立即返回。它只需要注册一个延续 与其余代码一起,在10毫秒后在线程池线程中运行。调用返回后,此代码在主线程中运行:

for (int i = 0; i < 1000; i++)
    Console.Write(".");

await longTask;

此代码在运行时,将向线程池发出信号以运行计划的继续。它开始在线程池线程中并行运行。

for (int i = 0; i < 1000; i++)
    Console.Write("<");

Console是线程安全的,这是一件好事,因为它同时被两个线程调用。如果不是这样,您的程序可能会崩溃!

答案 1 :(得分:1)

由于它们同时发生在不同的线程上。这完全取决于处理任务的上下文。您可以将其设置为一次仅执行一项任务,也可以多次执行。默认情况下,它处理多个。

在这种情况下,DoSomething()在您调用时开始运行。当打印出线程ID时,它表明在延迟之后,另一个线程开始打印<。通过在方法中使用await,它会创建另一个线程来运行它,然后退回到运行Main,因为没有await告诉它在继续之前要等待它。

至于代码将继续在同一线程上运行,该线程取决于给定的上下文以及是否使用ConfigureAwait。例如,ASP.NET没有上下文,因此无论线程何时可用,执行都会继续。在WPF中,默认情况下它将与启动时在同一线程上运行,除非使用ConfigureAwait(false)来告诉系统它可以在任何可用线程中运行。

在此控制台示例中,在等待DoSomething()之后,Main将继续其线程,而不是在测试用例中启动整个程序的线程。这是合乎逻辑的,因为控制台程序也没有上下文来管理哪个线程运行哪个部分。

这是修改后的代码,用于测试特定实现上发生的情况:

static async Task Main(string[] args)
{
    Console.WriteLine("Start: " + Thread.CurrentThread.ManagedThreadId);
    var longTask = DoSomethingLong();
    Console.WriteLine("After long: " + Thread.CurrentThread.ManagedThreadId);

    for (int i = 0; i < 1000; i++)
        Console.Write(".");

    Console.WriteLine("Before main wait: " + Thread.CurrentThread.ManagedThreadId);
    await longTask;
    Console.WriteLine("After main wait: " + Thread.CurrentThread.ManagedThreadId);
}

static async Task DoSomethingLong()
{
    Console.WriteLine("Before long wait: " + Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(10);
    Console.WriteLine("After long wait: " + Thread.CurrentThread.ManagedThreadId);
    for (int i = 0; i < 1000; i++)
        Console.Write("<");
}

在我的.NET Core 3.0预览版7中,它将显示以下内容:

  

开始:1
  不久之后:1
  漫长的等待之前:1
  经过漫长的等待:4
  在主要等待之前:1
  主要等待后:4

在我的系统上,最后一个可能是1或4,这取决于碰巧可用的线程。这显示了等待Task.Delay()的方式如何从DoSomethingLong()方法中退出,继续运行Main,并且在延迟之后,它需要另一个线程来继续运行DoSomethingLong(),因为原始线程正在忙于执行其他操作东西。

有趣的是(至少对我来说),即使直接与DoSomethingLong()运行await时,ManagedThreadId也会做同样的事情。我本来希望在那种情况下只有线程ID 1,但这并不是实现看起来的样子。