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

8

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

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的作用。我认为,如果执行到了await,那么执行就会返回给调用方法,并且等待任务在另一个线程中运行。在我的例子中,所有操作都在一个线程中执行,并且在await关键字之后,执行不会返回给调用方法。 真相何在?


你可以观看这个视频:https://channel9.msdn.com/Shows/Going+Deep/Mads-Torgersen-Inside-C-Async - Chaka
1
我认为如果执行到了await,那么执行会返回给调用者方法,等待的任务在另一个线程中运行。我所知道的每一个async介绍(包括我的)都会特别明确地说明这不是发生的事情。 - Stephen Cleary
4个回答

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

如果我们使用Thread.Sleep而不是Task.Delay,那么所有内容都将在同一个调用线程上运行。


@Jeksonic TCS 的使用是正确的。问题在于 .Net 中的 Begin/End 方法已经是异步的了。因此,当您调用 Begin 时,它只是启动一个操作并释放一个线程。只有当操作完成时,它才会调用提供的回调函数。 - i3arnon
请勿在异步代码上调用Task.Wait。http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - Gusdor
1
@Gusdor,你在Main函数内。这只是一个例子,没关系。 - i3arnon
1
谢谢回复,我填补了一些关于异步等待的理解空白。 - Jeksonic
@i3arnon,提问者如何知道什么时候不好? - Gusdor
显示剩余4条评论

1
为了真正理解这种行为,你需要首先了解什么是“Task”,以及“async”和“await”对你的代码实际上做了什么。
“Task”是“活动”的CLR表示。它可以是在工作池线程上执行的方法,也可以是从数据库上的网络检索数据的操作。它的通用性允许它封装许多不同的实现,但基本上你需要理解它只是意味着“一种活动”。
“Task”类提供了检查活动状态的方法:它是否已完成、它是否还没有开始、它是否生成了错误等。这种对活动的建模使我们更容易地组合构建程序,而不是一系列方法调用的序列。
考虑下面这个微不足道的代码:
public void FooBar()
{
    Foo();
    Bar();
}

这意味着“执行方法Foo,然后执行方法Bar”。如果我们考虑从FooBar返回Task的实现,则这些调用的组合是不同的。
public void FooBar()
{
    Foo().Wait();
    Bar().Wait();
}

现在的意思是“使用方法Foo启动一个任务并等待其完成,然后使用方法Bar启动一个任务并等待其完成。” 在Task上调用Wait()很少是正确的 - 它会导致当前线程阻塞,直到Task完成,并且在一些常用的线程模型下可能会导致死锁 - 因此,我们可以使用asyncawait来实现类似的效果,而不需要这个危险的调用。
public async Task FooBar()
{
    await Foo();
    await Bar();
}
async关键字会将方法的执行分解成多个块:每次写await时,它会将下面的代码生成一个“continuation”(续体):一个作为Task执行的方法,在等待的任务完成后执行。

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

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

  • 启动任务Foo()
  • 当该任务完成时,启动任务Bar
  • 当该任务完成时,指示方法任务已完成

在您的控制台应用程序中,您没有等待任何代表挂起IO操作的Task,这就是为什么您会看到一个被阻塞的线程 - 没有机会设置一个将异步执行的继续。
我们可以通过使用Task.Delay()方法来修复您的LongIOAsync方法,以异步方式模拟长时间的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);
}

0

简单来说,LongIOAsync()是阻塞的。如果你在一个GUI程序中运行它,你实际上会看到GUI会短暂地冻结 - 这不是async/await应该工作的方式。因此整个事情就崩溃了。

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


0

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

   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则适用于需要传递设置任务完成或其他状态的能力的情况。

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