Answers:
协程从未离开过,与此同时,它们只是被其他事物所笼罩。最近对异步编程以及协程的兴趣增加,主要是由于以下三个因素:功能编程技术的接受程度提高,对真正的并行性支持不佳的工具集(JavaScript!Python!),最重要的是:线程和协程之间的权衡取舍。对于某些用例,协程在客观上更好。
OOP是80年代,90年代乃至当今最大的编程范例之一。如果我们看一下OOP的历史,特别是Simula语言的发展,就会发现类是从协程演变而来的。Simula用于模拟具有离散事件的系统。系统的每个元素都是一个单独的流程,它将在一个模拟步骤的持续时间内响应事件而执行,然后屈服于让其他流程执行其工作。在Simula 67的开发过程中,引入了类概念。现在,协程的持久状态存储在对象成员中,并且通过调用方法来触发事件。有关更多详细信息,请考虑阅读Nygaard&Dahl 撰写的论文《 SIMULA语言的发展》。
因此,有趣的是,我们一直在使用协程,我们只是将它们称为对象和事件驱动的编程。
关于并行性,有两种语言:具有适当内存模型的语言和没有适当语言的语言。内存模型讨论诸如“如果我写一个变量,然后在另一个线程中从该变量中读取,我会看到旧值或新值还是无效值吗?“之前”和“之后”是什么意思?保证哪些操作是原子操作?”
创建一个好的内存模型很困难,因此对于大多数未指定的,实现定义的动态开源语言(Perl,JavaScript,Python,Ruby,PHP),从来没有做过这样的工作。当然,所有这些语言的发展都远远超出了最初为它们编写的“脚本”。好的,其中一些语言确实具有某种内存模型文档,但是还不够。相反,我们有一些技巧:
Perl可以在线程支持下进行编译,但是每个线程都包含一个完整的解释器状态的单独克隆,这使线程变得非常昂贵。唯一的好处是,这种无共享方法避免了数据争用,并迫使程序员仅通过队列/信号/ IPC进行通信。Perl对于异步处理没有很强的故事。
JavaScript一直对函数式编程提供了丰富的支持,因此程序员可以在需要异步操作的程序中手动编码连续性/回调。例如,对于Ajax请求或动画延迟。由于Web本质上是异步的,因此有很多异步JavaScript代码,并且管理所有这些回调非常痛苦。因此,我们看到了许多努力来更好地组织这些回调(Promise)或完全消除它们。
Python具有这种不幸的功能,称为全局解释器锁定。基本上,Python内存模型是“由于没有并行性,所有效果都会顺序出现。因此,虽然Python确实有线程,但它们仅与协程一样强大。[1] Python可以通过带有的生成器函数对许多协程进行编码yield
。如果使用得当,则仅此一项就可以避免JavaScript已知的大多数回调地狱。Python 3.5中更新的async / await系统使异步惯用语在Python中更加方便,并集成了事件循环。
[1]:从技术上讲,这些限制仅适用于CPython(Python参考实现)。其他类似Jython的实现确实提供了可以并行执行的真实线程,但是必须花很长时间才能实现等效的行为。本质上:每个变量或对象成员都是易失性变量,因此所有更改都是原子性的,可以在所有线程中立即看到。当然,使用易变变量比使用普通变量要昂贵得多。
我对Ruby和PHP知之甚少,无法正确烘焙它们。
总结一下:这些语言中的某些具有基本的设计决策,这些决策使多线程不受欢迎或无法实现,从而导致人们更加关注协程等替代方案以及使异步编程更方便的方法。
最后,让我们谈谈协程和线程之间的区别:
线程基本上类似于进程,除了进程内的多个线程共享一个内存空间。这意味着线程在内存方面绝不是“轻量级”。线程由操作系统抢先调度。这意味着任务切换具有高开销,并且可能会在不方便的时间发生。此开销包括两个部分:挂起线程状态的开销,以及在用户模式(对于线程)和内核模式(对于调度程序)之间切换的开销。
如果一个进程直接和协作地调度其自己的线程,则无需将上下文切换到内核模式,并且与间接函数调用相比,切换任务的成本相对较高,例如:相当便宜。根据各种细节,这些轻量级的线可以称为生线,纤维或协程。绿色线程/光纤的著名用户是早期的Java实现,而最近在Golang中使用Goroutines。协程的概念优势在于,可以根据在协程之间明确传递的控制流来理解它们的执行。但是,除非在多个OS线程之间调度这些协程,否则它们不会实现真正的并行性。
便宜的协程在哪里有用?大多数软件不需要大量的线程,因此通常昂贵的线程通常是可以的。但是,异步编程有时可以简化您的代码。要自由使用,此抽象必须足够便宜。
然后是网络。如上所述,Web本质上是异步的。网络请求只需要很长时间。许多Web服务器维护一个充满工作线程的线程池。但是,在大多数情况下,这些线程将处于空闲状态,因为它们正在等待某些资源,例如从磁盘加载文件时等待I / O事件,等待客户端确认响应的一部分或等待数据库查询完成。NodeJS惊人地证明了基于事件的异步服务器设计非常有效。显然,JavaScript并不是唯一一种用于Web应用程序的语言,因此,其他语言(在Python和C#中值得注意)也极大地推动了异步Web编程的发展。
协程过去非常有用,因为操作系统没有执行抢先式调度。一旦他们开始提供抢先式调度,则不再需要定期放弃程序中的控制。
随着多核处理器变得越来越普遍,协程被用于实现任务并行性和/或保持较高的系统利用率(当一个执行线程必须等待资源时,另一个线程可以开始在其位置运行)。
NodeJS是一种特殊情况,使用协程可以并行访问IO。即,多个线程用于服务IO请求,但单个线程用于执行javascript代码。在信号线程中执行用户代码的目的是避免使用互斥锁。如上所述,这属于试图保持系统利用率高的类别。
早期的系统使用协程提供并发,主要是因为它们是最简单的方式。线程需要操作系统的大量支持(您可以在用户级别上实现它们,但是您将需要某种方式安排系统定期中断您的进程),并且即使您确实有支持,也很难实现。
线程后来开始接管,因为在70或80年代,所有严肃的操作系统都支持它们(到90年代,甚至是Windows!),并且它们变得更加通用。而且它们更易于使用。突然,每个人都认为线程是下一件大事。
到90年代后期,裂缝开始出现,并且在2000年代初期,线程的严重问题变得显而易见:
随着时间的流逝,程序通常需要在任何时候执行的任务数量迅速增长,从而增加了由上述(1)和(2)引起的问题。处理器速度和内存访问时间之间的差距一直在增加,这加剧了问题(3)。而且,就程序所需的资源数量和种类的不同而言,程序的复杂性正在增长,从而增加了问题的相关性(4)。
但是,通过失去一点通用性,并给程序员增加一些额外的负担来考虑他们的流程如何一起运行,协程可以解决所有这些问题。
首先,我想说明一下协程不复活的原因,即并行性。通常,现代协程不是实现基于任务的并行性的手段,因为现代实现不利用多处理功能。最接近的是纤维。
现代协程已经成为实现惰性评估的一种方式,这在诸如haskell之类的功能语言中非常有用,在这种语言中,通过遍历整个集合而不是遍历整个集合来执行操作,您可以根据需要执行仅评估的操作(适用于项目的无限集或具有提前终止和子集的其他大型集)。
通过使用Yield关键字在Python和C#等语言中创建生成器(它们本身可以满足部分懒惰的评估需求),现代实现中的协程不仅是可能的,而且在语言本身没有特殊语法的情况下也是可能的(尽管python最终增加了一些帮助)。协同例程可以通过future s的概念来帮助实现懒惰的评估,如果将来您不需要变量的值,则可以延迟实际获取它的时间,直到您明确要求该值为止(允许您使用该值和懒惰地在与实例化不同的时间评估它)。
但是,除了懒惰求值之外,尤其是在Web领域,这些协同例程还有助于修复回调地狱。协程在数据库访问,在线事务,用户界面等方面变得很有用,在客户端机器上的处理时间不会导致更快地访问所需内容。线程可以完全填满同一件事,但是在这个领域需要更多的开销,并且与协程相反,实际上对于任务并行性很有用。
简而言之,随着Web开发的发展和功能范例与命令式语言的融合越来越多,协程已成为异步问题和惰性评估的解决方案。协程出现在问题空间中,在这里,多进程线程和一般情况下的线程是不必要,不便或不可能的。
在如JavaScript,Lua中,C#和Python语言协同程序都是由各个功能获得其实现放弃主线程等功能(无关的操作系统调用)的控制。
在这个python示例中,我们有一个有趣的python函数,其中包含一些await
内部函数。这基本上是一个收益,它将收益产生到loop
,然后允许运行不同的功能(在这种情况下,运行不同的factorial
功能)。请注意,当它说“并行执行任务”时,它实际上并没有并行执行,而是通过使用await关键字来执行其交织功能(请记住,这只是一种特殊的收益类型)
它们允许单个非平行,控制产量的并发处理这不是任务并行,在这个意义上,这些任务不工作永远在同一时间。协程不是现代语言实现中的线程。协例程的所有这些语言实现都是从这些函数yield调用派生的(程序员必须实际将其手动放入协例程中)。
编辑:C ++ Boost coroutine2以相同的方式工作,并且他们的解释应该更好地了解我在谈论yeilds,请参阅此处。如您所见,实现没有“特殊情况”,增强纤维之类的东西是该规则的例外,甚至需要显式同步。