C# async/await 控制台应用程序中的奇怪行为

C# async/await strange behavior in console app

我构建了一些 async/await 演示控制台应用程序并得到了奇怪的结果。代码:

class Program
{
    public static void BeginLongIO(Action act)
    {
        Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        act();
        Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
    }

    public static Int32 EndLongIO()
    {
        Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(500);
        Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return 42;
    }

    public static Task<Int32> LongIOAsync()
    {
        Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var tcs = new TaskCompletionSource<Int32>();
        BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        });
        Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return tcs.Task;
    }

    public async static Task<Int32> DoAsync()
    {
        Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var res = await LongIOAsync();
        Thread.Sleep(1000);
        Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return res;
    }

    static void Main(String[] args)
    {
        ticks = DateTime.Now.Ticks;
        Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        DoAsync();
        Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
    }

    private static Int64 ticks;
}

结果如下:

可能我没有完全理解await到底是什么。我认为如果执行等待,那么执行 returns 到调用方方法和等待在另一个线程中运行的任务。在我的示例中,所有操作都在一个线程中执行,并且执行不会 returns 到 await 关键字之后的调用方方法。 真相在哪里?

这不是 async-await 的工作方式。

将方法标记为 async 不会创建任何后台线程。当您调用 async 方法时,它 运行 同步直到一个异步点,然后才 returns 到调用者。

那个异步点是你 await 一个任务还没有完成的时候。当它完成时,该方法的其余部分将被安排执行。此任务应代表实际的异步操作(如 I/O 或 Task.Delay)。

在您的代码中没有异步点,没有返回调用线程的点。线程越来越深,并在 Thread.Sleep 上阻塞,直到这些方法完成并且 DoAsync returns.

举个简单的例子:

public static void Main()
{
    MainAsync().Wait();
}

public async Task MainAsync()
{
    // calling thread
    await Task.Delay(1000);
    // different ThreadPool thread
}

这里我们有一个实际的异步点 (Task.Delay) 调用线程 returns 到 Main 然后在任务上同步阻塞。一秒钟后,Task.Delay 任务完成,方法的其余部分在不同的 ThreadPool 线程上执行。

如果我们使用 Thread.Sleep 而不是 Task.Delay 那么它将全部 运行 在同一个调用线程上。

实际在后台线程上运行的行是

   Task.Run( () => {  } ); 

在您的示例中,您不是在等待该任务,而是在等待 TaskCompletionSource

   public static Task<int> LongIOAsync()
   {
        var tcs = new TaskCompletionSource<Int32>();

        Task.Run ( () => BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }));

        return tcs.Task;    
   }

当等待 LongIOAsync 时,您正在等待来自 tcs 的任务 是从给予 Task.Run()

的委托中的后台线程设置的

应用此更改:

    public static Task<Int32> LongIOAsync()
    {
        Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var tcs = new TaskCompletionSource<Int32>();

        Task.Run ( () => BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }));

        Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return tcs.Task;
    }

或者在这种情况下,您可以等待 Task.Run() 的返回, TaskCompletionSource 适用于您希望传递将任务设置为完成或其他方式的能力的情况。

简短的回答是 LongIOAsync() 正在阻塞。如果您 运行 在 GUI 程序中这样做,您实际上会看到 GUI 短暂冻结 - 这不是 async/await 应该工作的方式。因此整个事情都分崩离析了。

您需要将所有长 运行ning 操作包装在 Task 中,然后直接等待该 Task。在此期间不应阻止任何内容。

要真正理解这种行为,您需要首先理解 Task 是什么以及 asyncawait 对您的代码的实际作用。

Task 是 "an activity" 的 CLR 表示。它可能是在工作池线程上执行的方法。它可以是通过网络从数据库中检索某些数据的操作。它的通用性允许它封装许多不同的实现,但从根本上你需要理解它只是意味着 "an activity".

Task class 为您提供了检查 activity 状态的方法:是否已完成、是否尚未开始、是否产生错误等. activity 的这种建模使我们能够更轻松地编写作为一系列活动构建的程序,而不是一系列方法调用。

考虑这个简单的代码:

public void FooBar()
{
    Foo();
    Bar();
}

这意味着“执行方法 Foo,然后执行方法 Bar。如果我们考虑 returns Task 来自 Foo 的实现并且Bar,这些调用的组成是不同的:

public void FooBar()
{
    Foo().Wait();
    Bar().Wait();
}

现在的意思是 "Start a task using the method Foo and wait for it to finish, then start a task using the method Bar and wait for it to finish." 在 Task 上调用 Wait() 很少是正确的 - 它会导致当前线程阻塞,直到 Task 完成并可能导致死锁在一些常用的线程模型下 - 所以我们可以使用 asyncawait 来实现类似的效果而无需这个危险的调用。

public async Task FooBar()
{
    await Foo();
    await Bar();
}

async关键字导致你的方法的执行被分解成块:每次你写await,它都会接受以下代码并生成一个"continuation":一个方法在等待的任务完成后作为 Task 执行。

这与 Wait() 不同,因为 Task 没有链接到任何特定的执行模型。如果从 Foo() 返回的 Task 表示通过网络调用,则没有线程阻塞,等待结果 - 有一个 Task 等待操作完成。当操作完成时,Task 被安排执行 - 这个调度过程允许在 activity 的定义和执行它的方法之间进行分离,并且是任务使用的权力.

所以,方法可以概括为:

  • 开始任务Foo()
  • 当该任务完成时开始任务 Bar
  • 当该任务完成时表示方法任务已完成

在您的控制台应用程序中,您没有等待任何代表未决 IO 操作的 Task,这就是为什么您看到线程被阻塞的原因 - 永远没有机会设置将执行的延续异步地。

我们可以修复您的 LongIOAsync 方法,以使用 Task.Delay() 方法以异步方式模拟您的长 IO。此方法 returns a Task 在指定时间段后完成。这为我们提供了异步延续的机会。

public static async Task<Int32> LongIOAsync()
{
    Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);

    await Task.Delay(1000);       

    Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}