【问题标题】:C# async/await strange behavior in console app控制台应用程序中的 C# async/await 奇怪行为
【发布时间】:2015-10-12 09:24:08
【问题描述】:

我构建了一些 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 关键字之后执行不会返回到调用方方法。 真相在哪里?

【问题讨论】:

  • 你可以看这个:https://channel9.msdn.com/Shows/Going+Deep/Mads-Torgersen-Inside-C-Async
  • I thought if the execution comes to await then the execution returns to the caller method and task for awaiting runs in another thread. 我所知道的每一个async 简介(包括mine)都会特意明确说明这不是会发生什么。

标签: c# .net asynchronous async-await c#-5.0


【解决方案1】:

async-await 不是这样工作的。

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

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

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

举个简单的例子:

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

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

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

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

【讨论】:

  • 感谢您的快速回复!这很有帮助。然后我的下一个问题:对于从基于任务的包装器返回的任务,对于遵循 APM 模式(如来自 Socket 类的 BeginReceive 和 EndReceive)的方法,如何工作?此包装器使用 TaskCompletionSource 并返回已完成的任务。哪里有异步点?
  • @Jeksonic TCS 的使用很好。问题是 .Net 中的 Begin/End 方法已经是异步的。因此,当您调用Begin 时,它只会启动一个操作并释放一个线程。只有当操作完成时,它才会调用提供的回调。
  • @Jeksonic 在你的情况下。 Begin 方法只是阻塞调用线程,然后调用回调所以线程不会被释放。
  • @Gusdor 你在Main。在一个例子中。没关系。
  • 感谢回复,填补了我对async-await理解上的一些空白。
【解决方案2】:

在后台线程上实际运行某些东西的行是

   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 适用于您想要传递将任务设置为完整或其他方式的能力的情况。

【讨论】:

    【解决方案3】:

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

    您需要将所有长时间运行的操作包装在一个 Task 中,然后直接在该 Task 上等待。在此期间任何东西都不应阻塞。

    【讨论】:

      【解决方案4】:

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

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

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

      考虑一下这个简单的代码:

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

      这意味着“执行方法Foo,然后执行方法Bar。如果我们考虑一个从Foo和Bar返回Task的实现,这些调用的组成是不同的:

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

      现在的意思是“使用方法Foo 启动一个任务并等待它完成,然后使用方法Bar 启动一个任务并等待它完成。”在Task 上调用Wait() 很少是正确的——它会导致当前线程阻塞,直到Task 完成,并且在一些常用的线程模型下可能导致死锁——因此我们可以使用async 和await在没有这种危险调用的情况下达到类似的效果。

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

      async 关键字导致您的方法的执行被分解成块:每次您编写 await 时,它都会采用以下代码并生成一个“延续”:作为Task 执行的方法在等待的任务完成后。

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

      所以,方法可以总结为:

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

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

      我们可以修复您的 LongIOAsync 方法,使用Task.Delay() 方法以异步方式模拟您的长 IO。此方法返回一个在指定时间段后完成的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);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-10-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-05-04
        • 1970-01-01
        相关资源
        最近更新 更多