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

时间:2015-07-22 13:12:27

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

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

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;
}

结果如下:

enter image description here

也许我不完全明白究竟是什么等待。我想如果执行到来等待执行返回到调用方法和任务等待在另一个线程中运行。在我的示例中,所有操作都在一个线程中执行,并且执行不会在await关键字之后返回到调用方法。 真相在哪里?

4 个答案:

答案 0 :(得分:2)

这不是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线程上执行。

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

答案 1 :(得分:1)

要真正了解此行为,您需要先了解Task是什么以及asyncawait对您的代码实际执行的操作。

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

Task类为您提供了检查活动状态的方法:是否已完成,是否尚未启动,是否生成错误等。此活动建模使我们能够进行更多操作轻松编写构建为一系列活动的程序,而不是一系列方法调用。

考虑这个简单的代码:

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

这意味着“执行方法Foo,然后执行方法Bar。如果我们考虑从TaskFoo返回Bar的实现,这些电话的组成是不同的:

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

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

await

public async Task FooBar() { await Foo(); await Bar(); } 关键字导致您的方法的执行被分解为块:每次编写async时,它都会采用以下代码并生成“延续”:一个要执行的方法等待任务完成后,await

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

因此,该方法可以概括为:

  • 启动任务Task
  • 任务完成后,启动任务Foo()
  • 当该任务完成时表明方法任务已完成

在您的控制台应用程序中,您没有等待任何代表挂起IO操作的Bar,这就是您看到被阻塞线程的原因 - 从来没有机会设置一个异步执行的延续。

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

Task

答案 2 :(得分:0)

实际在后台线程上运行某些内容的行是

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

在您的示例中,您不是在等待Task,而是在等待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适用于您希望传递将Task设置为完整或其他方式的功能。

答案 3 :(得分:0)

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

您需要将所有长时间运行的操作包装在任务中,然后直接等待该任务。在那期间什么都不应该阻止。