协程有哪些使用场景?

63

协程的概念听起来很有趣,但是我不知道在实际的生产环境中是否有意义?哪些情况下使用协程实现更加优雅、简单或者高效呢?

9个回答

58

一个使用场景是具有多个同时连接、需要在所有连接中并行调度读写的Web服务器。

这可以使用协程来实现。每个连接都是一个协程,它读取/写入一些数据,然后将控制权交给调度程序。调度程序传递给下一个协程(执行相同的操作),循环遍历所有连接。


7
我不知道为什么这个没有得到+1的支持那么长时间。如果协程驱动的 Web 服务器假定协程正确设计用于分步计算,则在吞吐量方面将比重型线程 Web 服务器更出色,并且比状态机管理的服务器更容易理解。 - JUST MY correct OPINION
1
考虑到操作系统可以告诉您哪些连接需要关注,这似乎是一种低效的方法。 - xaxxon
@AliAfshar 协程的最佳应用之一! - anekix

39

应用场景:协程经常在游戏编程中用于时间分片计算。

为了保持游戏的一致帧率,例如60 fps,在每个帧中您大约有16.6ms来执行代码。这包括物理模拟、输入处理、绘制/绘画等操作。

假设您的方法在每个帧中都要执行。如果您的方法执行时间很长并跨越多个帧,则会导致游戏循环中的其余计算出现停滞,这将导致用户看到“卡顿”(帧速率突然下降)。

协程使得可以对计算进行时间分片,以便它在每个帧中都能运行一点点。

为此,协程允许该方法“yield”(暂停)计算,并将其返回给“调用者”(在本例中为游戏循环),以便在下次调用该方法时它可以从离开的地方继续执行。


谢谢,我已经花了一个小时来尝试解决这个问题,但是你的例子真的让我恍然大悟。 - kingfrito_5005
你能给一个更明确的用例示例吗? - stephanmg

29

Unix管道是一个使用案例:

grep TODO *.c | wc -l

上面的管道是一个协程。 grep 命令生成一系列行并将它们写入缓冲区。wc 命令从缓冲区读取这些行。如果缓冲区填满,则 grep 会“阻塞”,直到缓冲区为空。如果缓冲区为空,则 wc 等待缓冲区中的更多输入。
协程更常用于更受限制的模式,例如提到的 Python 生成器,或者作为管道。
有关更多详细信息和示例,请阅读维基百科文章,特别是 coroutinesiterators

2
我从未想过那个!太棒了! - geckos

20

真正的协程需要语言支持。它们需要由编译器实现,并由底层框架支持。

协程的一种基于语言支持的实现是C# 2.0中的yield return关键字,它允许您编写一个返回多个值以进行循环的方法。

然而,yield return有限制。该实现使用助手类来捕获状态,并且仅支持将协程作为生成器(迭代器)的特定情况。

在更一般的情况下,协程的优点在于它们使某些基于状态的计算更易于表达和理解。例如,将状态机实现为一组协程可能比其他实现更简洁。但是,这样做需要C#或Java尚不存在的语言支持。


是吗?在C#中,yield return用于迭代器方法中定义一个迭代器块。它允许方法返回一个值序列。这难道不和可以暂停和恢复的协程有所不同吗? - gingerbreadboy

16

协程非常有用,可用于实现生产者/消费者模式。

例如,Python引入了协程的语言特性称为生成器,旨在简化迭代器的实现。

协程还可用于实现协作式多任务处理,其中每个任务都是一个协程,会让出控制权给调度器/反应堆。


我不能评论Python的生成器,但之前我曾经使用过生成器结构。我发现这个概念在处理有趣的例题时是巧妙且非常实用的,但在实际编程中却非常难以使用。 - Paul Nathan
2
生成器在当今的Python中非常方便和广泛使用。它们可以产生比使用对象编写的等效代码更简单、更易读,将状态信息放入成员变量中。但是它们不是完整的协程,在某些方面存在局限性。 - bobince

13

当系统执行需要大量等待的长时间运行步骤,且包含两个或多个任务时,协程(coroutines)非常有用。

例如,考虑一个设备,它具有LCD和键盘用户界面以及调制解调器,并且需要使用调制解调器定期呼叫并报告其状态,而与键盘上的用户所做的操作无关。编写用户界面的最佳方式可能是使用像“input_numeric_value(&CONV_SPEED_FORMAT, &conveyor_speed);”这样的函数,该函数将在用户输入值时返回,并处理通信的最好方法可能是使用"wait_for_carrier();"这样的函数,该函数将在单元连接或确定不连接时返回。

如果没有使用协程,则UI子系统或调制解调器子系统必须使用状态机实现。使用协程可以使得两个子系统都以最自然的方式编写。请注意,重要的是,两个子系统都不能长时间处于不一致的状态并调用yield(),也不能在调用yield()之前不把事情放入“一致”状态。但通常很容易满足这些约束。

请注意,虽然可以使用全面的多任务处理,但这需要几乎在修改共享状态的任何地方都广泛使用锁或其他互斥构造。由于协程切换器除了在yield()调用时,永远不会切换事物,因此任何一个例程都可以自由地修改共享状态,只要它确保在下一次yield之前一切有序,并做好准备让另一个例程在yield() "期间" 修改状态。


你不是唯一一个提到状态机的人。为什么协程可以替代状态机?请用通俗易懂的语言解释。 - Iizuki
1
@Iizuki:一个人可以在协程中实现状态机,许多系统有足够简单的状态机,用协程替换所有这些状态机是很愚蠢的。使用协程的最大优点是可以以更加正常的方式编写代码。例如,如果一个人有一个“putchar”函数,该函数会在硬件准备就绪时向串行端口发送一个字节,否则只需执行任务旋转,则可以使用类似printf(“位置为(%d,%d)”,x,y);的内容,并且不会阻塞其他任务的执行。而使用状态机,则需要... - supercat
1
要么有一个可以容纳整个消息的缓冲区,要么有一个小缓冲区来处理每个十进制输出,并在适当的时间内使主状态机将x或y格式化到该缓冲区中。使用协程,额外缓冲区只需要在代码开始格式化数字时和完成后之间分配,而使用状态机可能需要静态分配。 - supercat

7
作为生产者/消费者的例子,可以使用协程来实现批量报告程序。
该示例的关键提示是需要有一些复杂的工作来消耗输入数据(例如解析数据或在账户上累积费用和付款),以及一些复杂的工作来生成输出。当具备这些特征时,可以采取以下措施:
- 如果您可以在各个地方编写工作单元,则很容易组织/理解输入端代码。 - 如果输出端代码可以在嵌套控制结构中读取下一个工作单元,则同样易于组织/理解输出端代码。
此时,协程和队列都是可供选择的好技术。

1

一个使用场景:内存数据库系统。

协程可用于减少因在此类数据库系统中进行查找(如哈希探测或B+树遍历)而导致的CPU停顿,从而提高性能。

数据库系统使用许多基于指针的数据结构,包括哈希表和B+树,这些结构需要在哈希探测或B+树遍历期间进行大量的“指针追踪”。当一批操作同时到达时,协程可以轻松地在操作之间交错指令流,从而减少由缓存未命中引起的CPU停顿,并利用内存访问的并行性。


1
一个快速浏览以上答案,让我感觉它们都关注于执行速度,而真正的协程另一个有趣的特点被忽略了:它们可以进行值传递。当然,在低级代码和Python中都是如此。
一个使用案例是将两个游戏程序相互对抗 - 每个程序都会向另一个程序提交下一步操作。优势在于代码的可读性更强:每个协程将另一个协程视为一个函数 - 发送一个操作并接收一个答案。类似的情况也出现在模拟和其他问题中。
相比之下,以上示例仅单向发送数据(生成器),如果有任何数据发送的话。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接