如何禁止任务被等待中断?
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 现在处于空闲状态,因此它被调度并且 运行 完成任务的剩余部分。
这在控制台应用程序中是正常的。 如果您需要细粒度地控制将延续安排到哪个线程 运行,那么您需要了解线程上下文的工作原理。 或者,编写一个应用程序具有消息泵,默认上下文会将所有内容保存在同一个线程中。
来自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 现在处于空闲状态,因此它被调度并且 运行 完成任务的剩余部分。
这在控制台应用程序中是正常的。 如果您需要细粒度地控制将延续安排到哪个线程 运行,那么您需要了解线程上下文的工作原理。 或者,编写一个应用程序具有消息泵,默认上下文会将所有内容保存在同一个线程中。