协程的好处有哪些?

在学 Lua, 基础是 Node 单线程的回调.. 觉得多线程很难懂
关注者
3,450
被浏览
718,847
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

我来说补充几个大家可能没有说到过的:

  • 1 历史上是先有协程,是OS用来模拟多任务并发,但是因为它是非抢占式的,导致多任务时间片不能公平分享,所以后来全部废弃了协程改成抢占式的线程。


  • 2 线程确实比协程性能更好。因为线程能利用多核达到真正的并行计算,如果任务设计的好,线程能几乎成倍的提高你的计算能力,说线程性能不好的很多是因为没有设计好导致大量的锁、切换、等待,这些很多都是应用层的问题。而协程因为是非抢占式,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力
  • 我们在x360、xbox1和ps4上做游戏的时候,开线程用来做数据加载、解压这种不需要或者很少需要数据同步的任务的时候效率杠杠的,而协程用来处理一些应用层逻辑调度的时候非常方便。官方文档也建议,协程只是为了老代码移植和兼容性,不推荐新代码使用。


  • 3 说协程性能好的,其实真正的原因是因为瓶颈在IO上面,而这个时候真正发挥不了线程的作用。


  • 4 协程的确可以减少callback的使用但是不能完全替换callback。基于事件驱动的编程里面反而不能发挥协程的作用而用callback更适合。想象一下用协程来写GUI的事件处理你怎么写。而nodejs那种io瓶颈单任务流程用协程的确很适合,但是也需要callback做补充。


  • 5 LUA的标准版5.1里协程有一个内伤,不能跨c函数切协程,而JIT版没有这个问题。但是ios上面又不能用jit所以我直接把协程禁了免得到时候其他平台都是好的,到ios上就出奇怪的问题。


  • 6 状态机用协程其实也有问题,比如状态里面嵌套子状态,再由子状态切换到其他状态的子状态,开销和代码都会变差,反而不如经典的状态机简单明了高效


  • 7 其实nodejs早就有第三方协程模块了,只是底层用的os的协程而,因此性能最多只有callback版本的80%左右,而且scale的很不好,但是代码是简单清晰多了。其实无论你是os的还是vm的,协程的开销必然比callback的开销大


差不多就补充这些。总之,协程不是万能药,选择合适的工具同样很重要



------------------ 2018年7月10日 更新--------------------------------

上面说的是有栈协程,没有特别指出,这里特别指出一下,抱歉。

最近又多了一些协程相关的问题跟大家分享一下。当然再次强调,这里不是黑协程,而是希望增加大家的认识,没有什么是完美的解决方案。

  • 协程也需要锁。写过golang的肯定知道,因为golang的协程对应系统协程是m:n关系,这种情况下被多个线程运行的协程访问共享资源还是有竞争的,需要手动加锁。libgo也提供了协程锁。那如果是node那种单线程的情况呢?或者lua只有一个vm的时候呢?其实加锁是因为竞争,所以关键看有没有竞争。如果三方库有线程主动调用进来,你还是要加锁的,当然情况很少而已。
  • 默认协程切换的顺序是没有保证的。如果你发送1,2,3个包,每个包分配一个协程处理,第一个包中途遇到io切换到了第3个包的协程,你逻辑处理包的顺序就变成了1,3,2。其实这个问题是可以解决的,比如lua的协程切换可以手动选择协程唤醒;golang稍微复杂点,需要用一些同步手段手动处理这个问题,应该也可以解决。
  • 协程安全。历史上在windows上用fiber的时候就有发生。就是某些功能对协程是不友好的,比如TLS,异常等。结果就导致很多库(包括巨硬自己的)都跟协程有兼容性问题(下面一点)
  • 兼容性问题。比如,了解一下golang的cgo就知道了。兼容性最好的目前应该是无栈协程了。libgo应该比golang对c库的兼容性好很多,但是需要保证hook了所有会block的系统函数才行。


关于无栈协程,相对有栈协程我比较喜欢无栈协程,因为轻量(我很喜欢轻量的东西),但是因为js的await需要引入异常,然后就会引入异常安全,引入错误处理的选择等问题,提高了开发人员门槛。

虽然我能列举更多的好处,比如代码简洁,错误处理简单,调试和维护方便等好处,但是对比带来的问题,而且如果新老风格混用的话,我和同事讨论下来觉得,老代码还是让事情简单点好,我不希望我的开发人员写代码的时候频繁纠结到底用异常还是错误码,到底该不该await,这里要不要try catch异常等。

当然如果是新项目的话,我会培训大家相关知识,然后都用一种风格写,那应该就好很多了。


关于简洁上,我虽然很认同golang的设计理念,但是我觉得有一点,完全简化所有的东西是不可能的。简洁其实是相对,复杂性也是相对的就好像现代人能处理的简单数学问题在古代人看来就非常复杂。问题的固有复杂性是不会消失的,只是可能从语言层面转移到了语言底层实现和库上面了而已。所以客观来说,大部分情况下,golang的大部分设计是成功的,但是有一些简化过头了,比如泛型、错误处理和包管理。
------ 来自马后炮的crazybie


------------------------------2019-12-28 更新---------------------------

  1. 有栈协程,在调用异步接口时,完全隐藏了异步的特性,导致:
    1. 有些功能无法在上层实现,比如超时、时间统计。如果是回调就很好办,构造一个转发的新回调即可,无栈协程也类似,只是稍微麻烦一点。
    2. 误用,比如过度使用一些开起来同步且轻量,实际很耗时的异步接口
  2. 无栈协程,如果需要和回调混用,导致细节太复杂,需要完全理解无栈协程内部的工作原理,隐形成本提高,否则会出现潜在隐藏问题
  3. 回调始终是最简单最基础最通用的解决方案,缺点就是
    1. 书写繁琐,但是用三方库将嵌套展开后,维护性可以接受。
    2. 错误处理和顺序流程不一致,要处理2个分支,1个是调用的错误,1个是回调错误。协程在错误处理上是最方便,统一,自然的。
  4. 对外兼容性上,回调是兼容性最好的方案,因为回调是所有语言都支持的基础类型。回调返回error不依赖异常也提高了兼容性。混合语言编程或者全栈开发时可以享受一些语法统一的好处。
  5. 线上工程,需要在性能、兼容性、稳定性、维护性、开发效率、上手度等方面进行综合考虑,所以回调的性价比其实不低,需根据具体项目来评估,不能一味相信某些技术吹嘘,理论和实战是有区别的。