IO绑定的异步任务不是异步执行的

时间:2017-04-18 09:59:13

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

我花了很多时间来理解异步编程原理。但有一点仍不清楚。我对此代码感到困惑:

    static async Task Method()
    {
        Console.WriteLine($"Method entered.");

        await Task.Delay(1000);
        Console.WriteLine($"Await 1 finished.");

        await Task.Delay(1000);
        Console.WriteLine($"Await 2 finished");
    }

    static int Main(string[] args)
    {
        Console.WriteLine($"Main started.");

        return AsyncContext.Run(() => MainAsync(args));
    }

    static async Task<int> MainAsync(string[] args)
    {
        var t = Method();
        Console.WriteLine("Thread starting sleep.");
        Thread.Sleep(10000);
        Console.WriteLine("Thread stopped sleeping");
        Console.WriteLine(t.IsCompleted ? "Method completed" : "Method not completed");
        await t;
        return 0;
    }

结果:

Main started.
Method entered.
Thread starting sleep.
Thread stopped sleeping
Method not completed
Await 1 finished.
Await 2 finished

据我所知,当主线程处于休眠状态时,应执行来自Method的IO绑定操作(导致Task.Delay模拟IO)并按顺序中断主线程以继续执行方法代码。 所以我希望看到:

Main started.
Method entered.
Thread starting sleep.
Await 1 finished.
Await 2 finished
Thread stopped sleeping
Method completed

我知道Thread.Sleep我要停止主线程。但据我所知,Method()不应该需要线程,因为它包含IO绑定操作。 任何人都可以解释我误解的地方吗?

我使用的AsynContext是(here)。

3 个答案:

答案 0 :(得分:3)

默认情况下,“await”捕获当前synchronization context并在原始上下文中生成延续。 如果上下文未定义,则在默认线程池(TaskScheduler.Default)中生成continuation。

我不熟悉AsyncContext,但它可能在一些同步上下文中生成MainAsync,并且由于Thread.Sleep阻塞占用该上下文的线程,“await”的延续将等到上下文被释放。

这不是一个奇怪的现象,你可以在没有AsyncContext类的情况下重现它。尝试在Windows窗体应用程序中运行相同的代码,您将看到。 Windows窗体具有自己的同步上下文,可防止不同步的控制操作。

要解决此问题,您可以通过使用ConfigureAwait(false)方法告诉“await”不捕获同步上下文。

static async Task Method()
        {
            Console.WriteLine($"Method entered.");

            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine($"Await 1 finished.");

            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine($"Await 2 finished");
        }

await不会尝试在现有上下文中生成continuation,而是会在线程池任务中生成它。

答案 1 :(得分:1)

  

为什么您的代码按预期正常运行?

在使用AsyncContext.Run时,您正在为控制台应用程序提供明确的上下文,否则会NULL Synchronization Context,当您在MainAsync中执行以下代码行时:

var t = Method();
Console.WriteLine("Thread starting sleep.");
Thread.Sleep(10000);

然后Method()开始执行,遇到声明的地方:

await Task.Delay(1000);

它将控制权交还给调用者,在那里你通过使线程休眠10秒Thread.Sleep(10000);来阻止上下文,所以现在在此睡眠结束之前,在Async方法中不能进行Continuation,因为它等待为了使Continuation上下文可用,当它是空闲的,然后它开始执行Continuation,但到那时它还完成了MainAsync中的其余语句,这些语句似乎被优先处理并且响应符合预期,它等待着只有在最后,事实上检查任何逻辑(如t.IsCompleted的任务状态更多的是代码气味,更好的只是await t,等待任务完成

  

有两种方法可以获得您期望的行为

  1. 如@Arik所示,配置两者等待使用ConfigureAwait(false),这意味着什么,简单地说,为了运行Async继续,它不需要原始上下文,并且将继续作为真正的异步操作,因此将提供您期望的结果。大多数库都具有异步功能,特别是基于IO的,实现ConfigureAwait(false)
  2. 从Main进行调用return MainAsync(args).Result;,这将确保控制台应用程序的标准行为,这意味着NULL Synchronization Context,这意味着Async不关心任何现有Context的延续,即使你正在进行Thread睡眠,它也会在后台进行,因为它不期望上下文,结果会与你期望的相同

    Main started.
    Method entered.
    Thread starting sleep.
    Await 1 finished.
    Await 2 finished
    Thread stopped sleeping
    Method completed
    0
    

答案 2 :(得分:0)

AsyncContext计划在单个线程上执行的所有任务。您的方法由Delays和WriteLines组成。您可能会认为Delays类似于IO操作,因为它们不需要执行线程。但是,WriteLine需要一个线程。因此,当Method从Delay唤醒时,它等待线程可用于执行WriteLine。

实际上,即使它不包含WriteLines而且只包含延迟,Method也会阻塞,因为它需要一个线程让Delay返回并启动新的Delay,但如果没有WriteLines则更难注意到。