【问题标题】:IO-bound async task not executes asynchronouslyIO 绑定异步任务不异步执行
【发布时间】:2017-04-18 09:59:13
【问题描述】:

我花了很多时间来了解异步编程原理。但有一件事还不清楚。我被这段代码弄糊涂了:

    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)并顺序中断主线程以继续执行 Method 代码。 所以我希望看到:

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

我知道 Thread.Sleep 我正在停止主线程。但据我了解 Method() 不应该需要线程,因为它由 IO 绑定操作组成。 谁能解释我在哪里误解它?

我正在使用的 AsynContext 是 (here)。

【问题讨论】:

  • “但据我了解,Method() 不应该需要线程,因为它由 IO 绑定操作组成” - 不,它不需要。 Task.Delay() 与 IOCP 无关。 stackoverflow.com/questions/32008662/…
  • @MickyD 但是 Task.Delay() 正在模拟 IO 异步操作
  • 我建议您再次阅读该链接。尽管Task.Delay() 模拟了异步操作,但并非所有异步操作都是“IO 绑定”的,Task.Delay() 当然也不是 IO 绑定
  • 每个 Async 调用都不需要 IO 绑定,这虽然是主要目的,但这里 Task.Delay 确实代表了真正的 Async 调用,它阻止了继续,而控制权交还给调用者,这会阻止它,因此继续也无法继续

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


【解决方案1】:

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

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

这不是一个奇怪的现象,你可以在没有 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 不会尝试在现有上下文中生成延续,而是在线程池任务中生成它。

【讨论】:

  • 这是正确的,有助于实现您需要的结果,因为您明确配置 await 建议可以忽略 Continuation 上下文
【解决方案2】:

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

在使用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 方法中进行继续,因为它等待继续上下文可用,当它空闲时,它开始执行继续,但到那时它还完成了MainAsync 中的剩余语句,这似乎是优先的并且响应如预期的那样,它仅在非常等待最后,实际上检查任务状态是否有类似t.IsCompleted 这样的逻辑更像是代码味道,更好的是只有await t,它等待任务完成

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

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

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

【讨论】:

    【解决方案3】:

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

    实际上,即使它不包含 WriteLines 而只包含延迟,该方法也会阻塞,因为它需要一个线程让延迟返回并开始新的延迟,但如果没有 WriteLines,这将更难以注意到。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多