百科问答小站 logo
百科问答小站 font logo



Golang、Kotlin、C#、JS、Python等都有协程,市面上的协程有什么本质上的区别? 第1页

  

user avatar   nan-47-77-64-47 网友的相关建议: 
      

全文私货,没一点干的:

协程就是协作式多任务,你可以把协作式用户态多线程,“有栈协程”称为协程,也可以把cps模拟的,或者加了糖的cps模拟的(js:你再骂???)协作式多线程称为协程,或者把基于异步运行时的async-await称为协程。

免责声明:js(现在)的promise和async-await不是cps模拟(虽然它可以是)


协程本质的特点是什么呢,是任务可以“让出”执行权,之后在合适的时机可以恢复执行。而如果任务不让出执行权,那么它便不会打断:这便是“协作式”的含义。

简单说就是“有让出无抢占”,就叫协程。

比如yield。实际上抢占式多线程本来就可以通过yield主动让出执行权,只不过抢占式多线程无法阻止自己被抢占。

await也是让出执行权的方式。


我们看到抢占式多线程减去抢占就等于协程,但是因为直接使用内核态多线程是不能阻止抢占的,所以这个思路实现的协程基本上都是内核线程池加协作式用户态线程。


为什么协程相比线程具有高性能,这不是因为上下文切换,协程同样需要上下文切换,用户态线程的上下文切换同样不走内核。协程高效的原因之一是,主动进行切换比通过抢占随时可能发生的切换需要保存更少的现场,之二是协程运行时无需实现抢占,就比如你不用timer,不用太多signal handler,这样的协程运行时岂不是好实现多了。

抢占是一个很低效的操作,“打断”这种操作不是那么容易做的,操作系统级别上之所以需要抢占是为了避免任务占着CPU不走。在多处理系统上这个问题实际上可以得到缓解,比如把bsp特供给内核,内核发现哪个任务画风不太对一个ipi过去就好了:不过这种方式显然比抢占更低效。

协程的一些好处也和这个协作式/无抢占有关。比如同步和竞争条件的问题。。。当然M比N的话那就没这回事了:不过避免抢占以提高性能还是一定的,因为你自己知根知底的代码还要去抢占,这基本上就是牺牲吞吐量换取响应时间的操作。


常见的协程实现的区别在调度上,或者更大了说在运行时。

比如用户态多线程运行时本身就有一个调度器,这个调度器会把就绪的协程池在内核线程池中分配。当然如果这个内核线程池不止一个。。。那么甚至可能有另一个协程在另一个核心上和你一起在运行,惊喜不惊喜(

异步运行时比如事件循环,它在实现方看来和用户态多线程没有太大区别,用户态多线程运行时调度的是就绪的线程,事件循环调度的是就绪的事件,也都差不多。如果运行时决定在多个核心上各跑一个事件循环,那么。。。

还有那么一种呢,它没有调度器,它需要用户自己来调度,你什么时候想让一个已经yield掉的协程恢复运行,你要自己去调用它的后续,这就,很有意思也很有用。

还有那么一种,它叫lua,它的协程是使用用户态线程实现的,但是它没有提供调度器,所以它是第三种。(提协程怎么能不提lua呢)

你要问哪个是真协程,怎么说呢,都是,反正我最喜欢无调度器的实现。


csharp的yield,运行时不创建额外的环境,那么yield直接返回到“直接”调用者,直接调用者可以级联yield返回到最终调用者,yield会将当前执行状态,或者可以看作continuation以某种方式保存,恢复时由外界手动调用continuation进行恢复。类似的还有js的generator,c++ 20的协程,这种协程实现对运行时无假设并且提供最细粒度的控制。

lua的coroutine,运行时需要创建用户态线程,yield会中断整个用户态线程返回到线程创建者,yield会将执行状态保存在线程上下文中,恢复由外界手动恢复该线程。这里的协程实现要求yield能够中断整个线程,这可能会引入一定的同步问题,因为上层调用者无法控制下层的yield。所以无栈协程虽然被认为具有传染性,但是我认为这种传染性反而是必要的。

然后async-await,通过await让出执行并返回到直接调用者,直接调用者可以级联await返回到最终调用者,await依然可以看作将运行状态保存为continuation,但是协程的恢复由异步运行时控制,await会将continuation传递给被等待的信号并最终传递给运行时,而非调用者。

yield想要模拟async-await实际上是可行的,即协程在yield的同时给出它想要等待的信号量,调用者在信号量改变后恢复协程执行,这等于手动实现了一个异步运行时。

有调度的有栈协程就是goroutine(的无抢占版),约等于用户态线程去掉抢占。


“任何异步操作都会有一个隐藏在背后的线程或是内核在帮你做事情”

这就不对了,同步操作才是内核在帮你做事。调用同步IO的API之后内核需要帮你把线程改成等待态,等IO完成之后内核再给你改回就绪态。而异步IO呢,内核做完了直接返回,IO结束给你发一个sigio就完了。

只能说操作系统的设计者在这个地方没想明白,异步IO对内核反而是更爽的。

不过,posix嘛。

当然epoll的确是内核在帮你做事了。


user avatar   haozhi-yang-41 网友的相关建议: 
      

先说明一下概念:协程的核心本质是用户态(应用代码)主动释放和获得cpu计算资源的一种架构——区别于传统内核级线程/进程进行上下文切换时,应用层是不可控且无感知的。所以,最极端的情况来说,在代码里到处加sleep(1),也可以算是实现了一个极为粗放的协程。

然后来说一下你的困惑:

  1. io密集型代码都是协程式的代码。哪怕是传统的“非协程”的epoll/select这种模型,当线程进入epoll/select挂起状态时,本质上也是在主动的释放时间片。
  2. 协程的好处不是“减少上下文切换”,它的真正好处是:“在可控的时机进行上下文切换”。什么意思呢?就是在传统内核进行切换的时候,因为应用层是无感知的,所以内核无法假设应用代码到底处于什么状态,所以切换时必须把整个上下文非常完整的保存。而如果由应用层来控制这种切换的时机,那么这些保存和恢复工作可以大大简化——极端点说,无栈协程甚至可以认为是没有额外的保护和恢复工作(理论上极致优化的无栈协程的切换性能应该接近一次虚函数调用)。
  3. cpu密集型程序也可能在协程基础上获得提升,原因上面已经说了。但是,如果设计的非常优秀的多线程/多进程cpu密集型程序,用协程所能获得的提升幅度可能会不大——例如说线程和cpu核唯一绑定,而且各线程的计算资源只共享读无共享写等(实际上不少高性能计算程序都会有意识的这么干)。
  4. 这个问题还是和问题2是类似的原因:因为传统并发程序中,应用层无法知道内核什么时候会把自己切出去,所以它在写代码时必须假定自己在执行任何语句时都可能发生切换——这就是并发保护的由来。而一个写得非常完善的协程应用代码,因为所有的切出和切入时机和条件都是确定的,所以一个设计良好的协程应用,哪怕在多线程环境下,也完全可以确定各协程的状态以避免冲突,因此可以完全去除并发保护(当然这是最极端理想情况)。但要做到这点,一般需要应用代码对协程调度策略进行深度干预(例如说通知调度器:协程A/B/C是互斥的)。而虽然协程的调度代码执行在用户态,理论上我们可以用比较低的成本做到这点(对比内核切换,我们在应用层真的无力干预)。然而,一般我们用的是现成的高度通用而且傻瓜化的协程库,而且还希望提供一套接近于传统的单任务流的代码编写方式,所以在实践上很难完美的做到这点(调度策略的复杂程度不是几个简单的关键字或者语法糖能囊括的)。这点也是那句名言:“没有银弹”——在并发环境下,你不可能不写自定义的调度策略就直接获得并发安全(传统的锁/条件变量等也是自定义的调度策略)。
  5. 我在问题1里也说了,本质上,epoll/select确实就是一种无栈协程,它们的“缺陷”在于和fd/io高度绑定,不够通用化而已。事实上远古时期,也有一种玩法是把各种非io事件通过一些手段(eventfd,甚至是自己给自己发一个字节等)模拟成io事件,从而统一在epoll的事件调度器中实现一个类似于协程式的运行系统。
  6. 不确定所谓的协程“真假”的具体定义,不发表意见。

user avatar   pansz 网友的相关建议: 
      

协程本来是指由编程语言自身负责调度任务,不借助操作系统与处理器功能的任务调度方式。某种意义上,协程的本质就是「非抢占式多任务」,它当且仅当任务主动让出控制权时进行调度。

这种调度从操作系统看起来其实就是同一个线程,只不过内部实现了「非抢占式多任务」,换句话说同时只能运行一个协程,其它协程只能等待,当前协程需要等待的时候主动把控制权让出来,让其它协程工作,这样就可以允许同一个线程内跑多个协程。

由于协程并不借助操作系统线程机制,所以开销比线程更小。当年的C10K问题最终的解决方案本质上就是协程思路,虽然并不都是用协程实现,但那些异步框架本质上就是一种手动协程的表现形式,使用epoll之类的机制相当于在同一个线程内跑多任务。所以协程思想提高性能是肯定的,毕竟它解决了C10K这个经典问题。

至于说协程减少上下文切换,这是当然的,因为都是同一个线程,同一个线程内当然减少了上下文切换。


Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程,如果你创建了四个协程会发现程序实际运行在四个线程而不是一个线程上。破坏了协程的定义。

当然,这样做反而有好处,毕竟能够更好的利用多核CPU。

如果你CPU是8核的,你创建100个Kotlin协程实际上也就只是用了8个线程。显然Kotlin协程也依然支持多个协程在同一个线程里边跑,在同一个线程内跑的多个协程看起来算是真协程,只不过由于Kotlin协程会优先把CPU处理器核心占满,那么在协程数不多的时候它被分配到线程中去,于是就退化成了线程,看起来就像是个假协程库了。




  

相关话题

  为什么汇编mov指令不能用lock前缀? 
  React.js有哪些设计缺陷? 
  2022 年,学完 JS 高级后的学习路线怎样规划? 
  JavaScript中对 function 的参数进行重新赋值的影响? 
  做开发你遇到最无理的需求是什么? 
  JavaScript 是什么? 
  公司规定所有接口都用 post 请求,这是为什么? 
  如何评价映兔科技 CTO 陈辉的文章《谈谈创业公司的技术选型》? 
  为什么Python,Go,Rust等新语言都不支持函数重载? 
  如何看待哔哩哔哩用Go语言重写所有的Java后台工程? 

前一个讨论
设计模式有何不妥,所谓的荼毒体现在哪?
下一个讨论
你赞同野生动物宠物化吗?为什么?





© 2024-12-22 - tinynew.org. All Rights Reserved.
© 2024-12-22 - tinynew.org. 保留所有权利