什么是协程?





5.00/5 (11投票s)
学习如何在您的项目中使用强大的编程技术进行多任务处理
引言
对所有事情都使用线程池是没有意义的。它会给操作系统调度程序带来压力,通常会因为涉及锁定而严重地使代码复杂化,并且由于同样的原因甚至会损害性能。而且,有时一个线程就太过分了。当我们需要多任务处理能力时,线程的替代方案是使用协作式多任务处理代码。本文旨在教您如何在您自己的代码中执行这种多任务处理的一种模式。
概念化这个混乱的局面
协程本质上是将它们的执行分解成多个部分的方法。每次调用协程时,都会执行任务的下一部分。它们本质上可以在执行过程中中断,并在下次调用时返回到中断的地方。
C# 将生成其自己的专用协程,在 C# 中称为迭代器,它使用 yield return
语句将执行分成多个部分。您可能知道,每次移动迭代器时,例程都会从 yield return
之后停止的地方继续执行。
这样的例程不是魔法。它们不是特殊的例程。C# 生成的迭代器的底层是一个协程中的状态机,其中每个步骤(直到 yield return
的每个部分)都是不同的状态。返回的 IEnumerator<T>
实现保存当前状态,每次它传递到协程 (由 MoveNext()
实现) 中,以便它知道在哪里再次开始执行。
编写这个混乱的程序
我们将从头开始编写我们自己的协程,然后向您展示如何“作弊”并让 C# 为您生成一个“足够好”的协程,尽管它有点笨拙/黑客行为。
下面的协程有点牵强,它从 1 数到 100 然后再回到 1。每次你调用它,它会返回系列中的下一个数字。我们不能在例程中使用循环来完成这个,所以我们在一个 switch case 下分解它,使用一个状态机
static int Coroutine1(Coroutine1Token token)
{
// switch on our state
switch(token.State)
{
case 0: // initially, we increment the value
++token.Value;
// .. until it's 100, then we go to state 1
if(100==token.Value)
token.State = 1;
break;
case 1:
// next, after we're done above we decrement the value
--token.Value;
// .. until it's 1, then we go to state 2
if (1 == token.Value)
token.State = 2;
break;
case 2:
// state 2 is just to tell us we're at the end
// which we signal by returning -1
return -1;
}
// finally, just yield the value we have currently before exiting
return token.Value;
}
你可能注意到的第一件事是 Coroutine1Token
。这个类主要做两件事。它保存我们需要传递给函数的任何参数,在这种情况下我们不需要,以及任何工作状态(例如本例中的 token.Value
),以及 token.State
整数本身,它跟踪我们在协程中的位置。
以下是它的调用方式
var tok = new Coroutine1Token();
int c;
Console.WriteLine("Coroutine1():");
while (-1 != (c = Coroutine1(tok)))
Console.Write(c.ToString() + " ");
Console.WriteLine();
在这里,我们创建一个新的 Coroutine1Token()
,Coroutine1()
需要它才能运行。然后我们在一个循环中调用 Coroutine1()
(就像你可能对任何协程所做的那样),让它处理每个部分,并在必要时更新 Coroutine1Token
中的状态。循环的每次迭代,我们都写入从 Coroutine1()
返回的值。
显然,你会在每个状态下做一些更有用的事情。但是,正如你所看到的,由于状态机,构建例程有点复杂。如果我们愿意处理它的一种丑陋的接口,我们可以让 C# 编译器使用迭代器来完成所有繁重的工作。输入 Coroutine2()
static IEnumerable<int> Coroutine2()
{
// at the first state we count from 1 to 100, yielding each value
for (var i = 1; i <= 100; ++i)
yield return i;
// at the next state we count from 99 to 1, yielding each value
for (var i = 99; 0 < i; --i)
yield return i;
// the final state is implicit, handled by the C# compiler
}
这与 Coroutine1()
做完全相同的工作并返回相同的结果。正如你所看到的,创建它更直观一些。缺点是使用起来不是很直观。此例程会导致 C# 编译器在底层生成非常类似于 Coroutine1()
的内容。它展开循环并在它们包含 yield
语句时将它们分解,并对 if
块等执行类似的操作。最终结果是一种不那么复杂的方式来创建状态机和驱动它的协程。然而,接口还有待改进。这是我们调用它的方式
Console.WriteLine("Coroutine2():");
foreach (var i in Coroutine2())
Console.Write(i.ToString() + " ");
Console.WriteLine();
看看我们是如何必须使用 foreach
来调用它的吗?这有点奇怪,特别是在你需要一个 while
循环或其他东西而不是使用 foreach
的情况下。在这些情况下,你必须直接使用枚举器
Console.WriteLine("Coroutine2() using while:");
using (var e = Coroutine2().GetEnumerator())
{
// each time MoveNext() is called, Coroutine2() is run for a single step
while(e.MoveNext())
{
// e.Current holds the result of Coroutine2()'s step
Console.Write(e.Current.ToString() + " ");
}
}
Console.WriteLine();
明白我说的不是很直观的意思了吗?这就是你为易于实现而付出的代价。
那么,所有这些都很好,但是它与多任务处理有什么关系呢?
本质上,每次我们调用一个协程时,它都会执行一个(希望是)少量的工作,然后将时间让给调用者。因为它以细粒度的块让出时间,这意味着它不会在持续时间内锁定你的调用线程。因此,你可以调用一个协程来完成一项工作片段,然后继续做下一件事,甚至是其他协程。
关注点
纤程是另一种抽象这一点的方式,我在我的 Lex 项目中使用了该技术。它们就像是微小的精简线程,以协作方式而不是抢占方式进行调度。
历史
- 2020 年 3 月 19 日 - 首次提交