如何禁止任务被等待中断?

How to disallow Task from being interrupted by await?

来自JavaScript 我已经习惯了代码执行不会交错,await 表达式是一个例外,它定义了固定的重入点。
.NET 异步语义似乎有所不同:在 Thread.Sleep 的每次调用期间,等待 运行 的 Task 跳入并执行工作。然后,Sleep 调用的 return 上,Task 的计算立即中断,如下面的代码所示。

如何实现循环的原子执行? (没有锁之类的;假设是单线程)

static async Task RunExperiment()
{
    var task = DoNotInterruptMe();
    Console.WriteLine("sleep...");
    Thread.Sleep(60);
    Console.WriteLine("sleep...");
    Thread.Sleep(60);
    Console.WriteLine($"Interrupted, because i={i}");
    Console.WriteLine("sleep...");
    Thread.Sleep(100);
    Console.WriteLine($"Interrupted again, because i={i}");
    await task;
    Console.WriteLine("task.Status: "+task.Status);
}

private static int i;

static async Task DoNotInterruptMe()
{
    Console.WriteLine("I: delay...");
    await Task.Delay(100);
    Console.WriteLine("I: working...");
    int j = 1;
    for (i = 0; i < 1<<30; i++)
    {
        j *= i;
    }
    Console.WriteLine("I: finished work");
}

运行 以上使用

RunExperiment().Wait();

我在这里使用的是 Thread.Sleep,但是 await Task.Delay(60) 的行为是一样的。

示例输出:

I: delay...
sleep...
sleep...
I: working...
Interrupted, because i=358449
sleep...
Interrupted again, because i=8598631
I: finished work
task.Status: RanToCompletion

根据您对 Console.WriteLine 的使用情况,我认为这是一个控制台应用程序。 Await 在控制台应用程序中的行为与在 windows 表单应用程序中的行为不同

让我们退一步说说 await 做了什么:

  • 检查可等待(在本例中为任务)是否完成。
  • 如果异常完成,抛出异常,让当前线程处理。
  • 如果正常完成,获取它的值,如果有的话,继续当前线程。
  • 如果未完成,将方法的剩余部分调度到运行 作为任务的延续,当任务完成时绑定到当前线程上下文完全的。然后 return 给你的来电者。

这是您感到困惑的最后一点。 在控制台应用程序中,任务的继续可能会被默认控制台上下文安排到工作线程上

在 windows 表单应用程序中,默认上下文是安排当前公寓的继续,我们可以这样做,因为我们 post 向该公寓的消息循环发送一条消息,导致继续到 运行。但是控制台应用程序中没有消息循环。

让我们通过重写您的程序来证明这一点:

static void Write(string s)
{
    Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:{s}");
}

现在我们每隔一个 Console.WriteLine 更改为调用 Write 和 运行 您的程序,我们看到:

1:I: delay...
1:sleep...
1:sleep...
6:I: working...
1:Interrupted, because i=4061320
1:sleep...
1:Interrupted again, because i=26763596
6:I: finished work
6:task.Status: RanToCompletion

线程 1 开始延迟,然后我们将该工作流的其余部分安排到延迟完成后 运行。我们立即 return 睡觉。同时,延迟完成并向 运行 其工作流的其余部分发送信号,它在线程 6 上执行。

回到线程 1,睡眠结束,我们发现 i 已更新——我注意到您忘记使 i 易变,但幸运的是我们得到了更新。

线程 1 然后等待与异步工作流关联的任务,该任务仍在线程 6 上 运行ning,因此它 returns 给它的调用者,并签署其工作流的其余部分作为该任务的延续。

该任务然后在线程 6 上完成,并且现在可以将继续计划 安排到工作线程 上。但是线程 6 现在处于空闲状态,因此它被调度并且 运行 完成任务的剩余部分。

这在控制台应用程序中是正常的。 如果您需要细粒度地控制将延续安排到哪个线程 运行,那么您需要了解线程上下文的工作原理。 或者,编写一个应用程序具有消息泵,默认上下文会将所有内容保存在同一个线程中。