我正在学习async / await,并且对MSDN的await的解释感到困惑:“ await操作符将暂停执行,直到GetByteArrayAsync方法的工作完成为止。同时,控制权返回给GetPageSizeAsync的调用者 ”
我不明白的是,“归还”是什么意思?首先,我相信当一个线程(例如UI线程)到达“ await”关键字时,系统将创建一个新线程(或从threadPool获取一个线程)来执行其余代码,UI线程可以返回到调用方方法并执行其余的操作。
但是现在我知道“等待”将永远不会创建线程。
我编写了一个演示:
class Program
{
static void Main(string[] args)
{
new Test().M1();
Console.WriteLine("STEP:8");
Console.Read();
}
}
class Test
{
public async void M1()
{
Console.WriteLine("STEP:1");
var t = M2();
Console.WriteLine("STEP:3");
await t;
}
public async Task<string> M2()
{
Console.WriteLine("STEP:2");
string rs = await M3();//when the thread reaches here,why don't it return to M1 and execute the STEP:3 ??
Console.WriteLine("STEP:7");
return rs;
}
public async Task<string> M3()
{
Console.WriteLine("STEP:4");
var rs = Task.Run<string>(() => {
Thread.Sleep(3000);//simulate some work that takes 3 seconds
Console.WriteLine("STEP:6");
return "foo";
});
Console.WriteLine("STEP:5");
return await rs;
}
}
此演示打印一些表示执行流程的标签,我认为应该是
STEP:1
STEP:2
STEP:3
STEP:4
STEP:5
STEP:6
STEP:7
STEP:8
(按顺序从1到8)
,但实际结果是: Figure 1
STEP:1
STEP:2
STEP:4
STEP:5
STEP:3
STEP:8
STEP:6
STEP:7
如果像MSDN的说明一样,控制权返回到M1并打印“ STEP 3”,那绝对是我错的,那到底是怎么回事?
预先感谢
答案 0 :(得分:6)
仅是为了解决问题,我们无法保证在任何特定的await
时刻,我们将继续进行 之外的其他代码。但是,在MSDN中的示例和您自己的示例中,我们将始终在每个await
点等待。
因此,我们到达await z
的某个位置,因此我们决定要等待。这意味着a)z
尚未完成(z
完成并不意味着我们目前不在乎此事)b)我们没有任何有用的工作现在做自己。
希望从上面可以看出,为什么不需要“创建新线程”或诸如此类的事情,因为就像我刚才所说的,没有要做的有用工作。
在我们从方法中返回控制之前,我们要加入 continuation 。 async
机制能够表达“ z结束后,请安排 this 方法的其余部分从我们的await
点开始”。
在这里,诸如同步上下文和ConfigureAwait
之类的东西变得很重要。我们正在运行的线程(以及我们将要放弃控制的线程)在某种程度上可能是“特殊的”。它可能是UI线程。它目前可能具有对某些资源的独占访问权限(请考虑ASP.Net核心前请求/响应/会话对象)。
如果是这样,希望提供特殊功能的系统已经安装了同步上下文,并且通过它我们还可以获得“ 何时继续执行此方法,我们需要具有与以前相同的特殊情况”。所以例如我们可以继续在UI线程上重新运行方法 。
默认情况下,在没有同步上下文的情况下,将找到线程池线程来运行我们的延续。
请注意,如果一个方法包含多个需要等待的await
,则在第一次等待之后,我们将以链状连续体的形式运行。我们的原始调用方在我们await
编辑的第一次 中获得了上下文。
在您的示例中,我们有3个await
点,并且所有3个 将等待并导致我们将控制权交还给调用方(而且我们没有多个{{ 1}} s中的一种来担心这两种情况。最后,所有这些等待都在等待await
完成(现代Thread.Sleep
代码中格式较差,应改用async
)。因此,在该方法中,在TaskDelay
之后出现的所有Console.WriteLine
都会被延迟。
但是,await
是M1
,最好避免在事件处理程序之外使用。 async void
方法存在问题,因为它们无法确定何时完成。
async void
调用Main
,它打印M1
,然后调用1
。 M2
打印M2
,然后调用2
。 M3
打印M3
,创建新的4
,打印Task
,然后 it 才能放弃控制。此时,它将创建一个5
来表示其最终完成,并从中获取TaskCompletionSource
并返回它。
请注意,只有在返回Task
的呼叫 时,我们才能击中M3
中的await
。我们还必须在这里等待,因此我们几乎与M2
相同,并返回M3
。
现在Task
终于有了M1
可以使用的Task
。但在此之前,它会打印await
。它将控制权返回给3
,现在将打印Main
。
之后,8
中Thread.Sleep
中的rs
运行完毕,我们打印了M3
。 6
将返回的M3
标记为已完成,没有更多工作要做,因此退出时没有安排任何继续。
Task
现在可以恢复并恢复其M2
并打印await
。完成此操作后,其6
就完成了,Task
最终可以恢复并打印M1
。
7
没有M1
标记为完成,因为它是Task
。
答案 1 :(得分:2)
最简单的方法是查看代码在后台实际执行的操作。这是该代码的大致翻译:
public void M1()
{
Console.WriteLine("STEP:1");
var t = M2();
Console.WriteLine("STEP:3");
t.ContinueWith(_ =>
{
//do nothing
});
}
public Task<string> M2()
{
Console.WriteLine("STEP:2");
Task<string> task = M3();
return task.ContinueWith(t =>
{
Console.WriteLine("STEP:7");
return t.Result;
});
}
public Task<string> M3()
{
Console.WriteLine("STEP:4");
var rs = Task.Run<string>(() =>
{
Thread.Sleep(3000);//simulate some work that takes 3 seconds
Console.WriteLine("STEP:6");
return "foo";
});
Console.WriteLine("STEP:5");
return rs.ContinueWith(t => t.Result);
}
请注意,为了简洁起见,我在此处未进行适当的错误处理。使用await
的最大好处之一是,它以您通常希望的方式处理错误,并且使用我在这里没有的工具...没有。这段代码也没有安排继续使用当前的同步上下文,这与这种情况无关,但是是有用的功能。
在这一点上,希望对发生的事情更加清楚。对于初学者来说,M1
和M3
实际上根本没有生意async
,因为他们在延续过程中从未做过任何有意义的事情。实际上,它们不应该只是async
并返回通过调用其他方法获得的任务。 M2
是唯一在其创建的任何延续中实际执行任何有用操作的方法。
这也使操作顺序更清晰。我们调用M1
,调用M2
,调用M3
,调用Task.Run
,然后M3
返回与Task.Run
相同的任务返回并返回自身,然后M2
添加其延续并返回,然后M3
执行下一个打印行,然后添加其延续(最终运行时不执行任何操作),然后返回。然后在稍后的某个时间点Task.Run
完成,并且所有延续都自下而上运行(因为每个都是另一个的延续)。