我想我已经阅读了大约20篇关于异步/等待的文章,关于SO的多个问题,但在理解其工作方式方面仍然存在很多差距,尤其是涉及多个线程以及何时全部使用一个线程完成的时候。
我为这段代码编写了自己,以测试一种情况:
static async Task Main(string[] args)
{
var longTask = DoSomethingLong();
for (int i = 0; i < 1000; i++)
Console.Write(".");
await longTask;
}
static async Task DoSomethingLong()
{
await Task.Delay(10);
for (int i = 0; i < 1000; i++)
Console.Write("<");
}
我明确地让Main继续将点写入控制台,同时在DoSomethingLong内部延迟执行被阻止。我看到延迟结束后,点写入和<符号写入开始相互干扰。
我只是想不通:是两个线程同时进行这项工作吗?一个写点,另一个写其他字符?还是以某种方式在这些上下文之间切换?我会说这是第一选择,但我仍然不确定。使用异步/等待时,何时可以期望创建其他线程?我仍然不清楚。
也总是执行Mai,到达等待状态然后再回写点的相同线程吗?
答案 0 :(得分:2)
var longTask = DoSomethingLong();
此行创建一个Task
。与Task.Run()
创建的任务相比,这是一个不同的任务。这是基于状态机的任务,由C#编译器自动生成。这是编译器生成的内容(从sharplab.io复制粘贴):
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading.Tasks;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default
| DebuggableAttribute.DebuggingModes.DisableOptimizations
| DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints
| DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class Program
{
[CompilerGenerated]
private sealed class <DoSomethingLong>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
private int <i>5__1;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Delay(10).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<DoSomethingLong>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
<i>5__1 = 0;
while (<i>5__1 < 1000)
{
Console.Write("<");
<i>5__1++;
}
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
// ILSpy generated this explicit interface implementation from
// .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
// ILSpy generated this explicit interface implementation from
// .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<DoSomethingLong>d__0))]
[DebuggerStepThrough]
private static Task DoSomethingLong()
{
<DoSomethingLong>d__0 stateMachine = new <DoSomethingLong>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
}
实际上,发生的情况是DoSomethingLong
方法的第一部分是同步执行的。它是第一个await
之前的部分。
在这种特定情况下,第一个await
前没有任何内容,因此对DoSomethingLong
的调用几乎立即返回。它只需要注册一个延续
与其余代码一起,在10毫秒后在线程池线程中运行。调用返回后,此代码在主线程中运行:
for (int i = 0; i < 1000; i++)
Console.Write(".");
await longTask;
此代码在运行时,将向线程池发出信号以运行计划的继续。它开始在线程池线程中并行运行。
for (int i = 0; i < 1000; i++)
Console.Write("<");
Console
是线程安全的,这是一件好事,因为它同时被两个线程调用。如果不是这样,您的程序可能会崩溃!
答案 1 :(得分:1)
由于它们同时发生在不同的线程上。这完全取决于处理任务的上下文。您可以将其设置为一次仅执行一项任务,也可以多次执行。默认情况下,它处理多个。
在这种情况下,DoSomething()
在您调用时开始运行。当打印出线程ID时,它表明在延迟之后,另一个线程开始打印<。通过在方法中使用await,它会创建另一个线程来运行它,然后退回到运行Main
,因为没有await
告诉它在继续之前要等待它。
至于代码将继续在同一线程上运行,该线程取决于给定的上下文以及是否使用ConfigureAwait
。例如,ASP.NET没有上下文,因此无论线程何时可用,执行都会继续。在WPF中,默认情况下它将与启动时在同一线程上运行,除非使用ConfigureAwait(false)
来告诉系统它可以在任何可用线程中运行。
在此控制台示例中,在等待DoSomething()
之后,Main
将继续其线程,而不是在测试用例中启动整个程序的线程。这是合乎逻辑的,因为控制台程序也没有上下文来管理哪个线程运行哪个部分。
这是修改后的代码,用于测试特定实现上发生的情况:
static async Task Main(string[] args)
{
Console.WriteLine("Start: " + Thread.CurrentThread.ManagedThreadId);
var longTask = DoSomethingLong();
Console.WriteLine("After long: " + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 1000; i++)
Console.Write(".");
Console.WriteLine("Before main wait: " + Thread.CurrentThread.ManagedThreadId);
await longTask;
Console.WriteLine("After main wait: " + Thread.CurrentThread.ManagedThreadId);
}
static async Task DoSomethingLong()
{
Console.WriteLine("Before long wait: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(10);
Console.WriteLine("After long wait: " + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 1000; i++)
Console.Write("<");
}
在我的.NET Core 3.0预览版7中,它将显示以下内容:
开始:1
不久之后:1
漫长的等待之前:1
经过漫长的等待:4
在主要等待之前:1
主要等待后:4
在我的系统上,最后一个可能是1或4,这取决于碰巧可用的线程。这显示了等待Task.Delay()
的方式如何从DoSomethingLong()
方法中退出,继续运行Main,并且在延迟之后,它需要另一个线程来继续运行DoSomethingLong()
,因为原始线程正在忙于执行其他操作东西。
有趣的是(至少对我来说),即使直接与DoSomethingLong()
运行await
时,ManagedThreadId
也会做同样的事情。我本来希望在那种情况下只有线程ID 1,但这并不是实现看起来的样子。