问一个协程方面的问题

发布于 2021-12-13 15:04:45

协程究竟解决了什么问题,都在吹协程,像是 go 、kotlin 都有协程,java 本身没有协程,

还有相比于线程,协程的优势是什么,为啥 java 没有协程,性能框架和 go 框架不相上下,协程这么牛逼为啥 rust 没有内置协程(听说都已经写好了,但是不符合理念,没合并)

操作系统调度的时候是以线程为单位调度的,又不知道协程的存在,即使你用协程了,操作系统还是只知道你是线程啊。

各种博客,都只说了比线程更轻量,占用内存小,切换成本小,但是线程切换是操作系统决定的,系统切换线程,你协程也没用啊。

各位大佬,救救孩子吧。

第 1 条附言  ·  3 天前
我看楼下不少说是协程调度的问题,和线程切换的问题, 我不理解的地方是 为什么要切换协程啊,

我理解的是如果你有阻塞问题,可以切到其他的协程。

但是感觉这样不对。如果阻塞是系统调用阻塞,线程就会挂起,调度到其他的线程了,你协程没用,如果不是系统阻塞,那我完全可以在线程内执行其他的,后面再回来执行
Questioner
kingofzihua
Viewed
0
100 个回答
virus94 2021-12-13 15:07:41
同疑惑,帮顶
v2exblog 2021-12-13 15:09:29
等待大神解答
cityboy 2021-12-13 15:10:15
相当于自己在一个线程了完成了多个协程的切换,不需要系统进行一些状态的切换,线程切换需要耗费一点的性能。看你项目的使用场景,如果本身项目是多线程的项目,通过协程确实可以提高一些效率。如果本身项目就不是多线程的,就没必要切换到协程。
kingofzihua 2021-12-13 15:11:49
@cityboy 我一个线程内为啥要进行多个协程切换?
nebkad 2021-12-13 15:12:35
可以从跨线程调用、回调函数的应用场景出发思考这个问题。
甚至再简单一点,生成一个无限的 Fibonacci ,如何写代码才最贴近它的数学定义。
airplayxcom 2021-12-13 15:13:02
洗牌 码牌 胡了 结算
emeab 2021-12-13 15:14:31
@kingofzihua 因为任务.. 有些任务不需要那么重的线程 那我就可以起一个协程来用.
anonydmer 2021-12-13 15:17:18
楼主需要了解的是线程的切换需要哪些成本。
cclin 2021-12-13 15:18:33
我的理解是把任务调度从系统交还给程序员自己。
线程的调度是由操作系统来控制的,协程则是程序主动让出资源。所以协程性能会好点。
不知道理解的对不对,有没有大佬解惑。
shyrock 2021-12-13 15:18:37
我理解协程是非抢占式的多任务调度,并且由应用开发者自己维护其调度逻辑。
这样就可以由应用开发者根据业务具体需要有针对性地定制调度逻辑,而不是只能适应系统的调度逻辑。
cityboy 2021-12-13 15:19:27
@cityboy 这个问题要重源头开始想,例如我为什么要使用一个线程?我可能用线程去跑一个简单上报的任务。如果有线程跑,就涉及到了线程的切换。那我在自己的线程里面,起一个协程去跑,即不用涉及系统的资源切换,又不会阻碍我主线程的任务。
niubee1  2 2021-12-13 15:19:38
协程主要针对的场景是 IO 阻塞的时候能够有轻量高效的处理大批量并发任务。对于大量并发的 CPU 开销(运算)来说,协程毫无用处,因为协程并不能协调 CPU 资源,比如控制 ffmpeg 压缩视频这事,你用协程就没办法,因为 CPU 核就这么几个,你并发上万个协程也没法变出更多的 CPU 资源出来,反而会因为过多的切换而更慢。
cloud107202 2021-12-13 15:19:53
协程适用于 IO 密集场景,CPU 密集场景的并行还是只能靠线程来 比如 folk-join pool + work stealing
协程还有个好处是结构化并发,代码可以线性书写,类似写同步逻辑一样


去补补基础好不?你这 rust 部分听说的也太对
ch2 2021-12-13 15:20:19
@kingofzihua #4 为了解决这个场景下的问题:一个任务按先后顺序可以拆解成很多个子任务,每个子任务占用 CPU 的时间又非常短,运行不了几个微秒就主动让出 CPU 了。当同时存在数以万计的子任务的时候,用线程来解决此类问题会造成 CPU 一直在忙于切换线程,真正执行任务的时间片反而成了次要的占比。
协程并不是解决这种场景下的问题的:任务本来就是 CPU 密集型的,每个子任务都在争抢 CPU ,用于切换线程的损耗可以忽略不计。
crazywhalecc  7 2021-12-13 15:20:25
举个不恰当的例子,你是个打工人,你每天工作 8 小时,要做的事情有编译程序 A 、编译程序 B 、帮老板端茶倒水、收发邮件。

你相当于一个线程(或进程),老板交给你的工作相当于系统分配线程的任务。

而协程就是,你自己怎么去更高效完成自己的任务。如果没有协程,就相当于“编译 A->编译 B->帮老板端茶倒水->收发邮件”,而协程就相当于“编译 A 开始->跑去给老板倒水->收发邮件->编译 B 开始->编译 A 结束->编译 B 结束”,可以节省你大量你坐在电脑前但是没有实际工作的时间去做其他事情。(对应协程中就是,比如遇到 HTTP API 请求,需要 0.5 秒,可以让这个进程继续先干别的,请求后回来继续当前处理完 HTTP API 请求的上下文)。

和线程的区别?多协程可以有效利用单核心计算,避免过多的 IO 等待,多进程(线程)可以有效利用多核心计算,避免单核负载过高,不是一回事。
kingofzihua 2021-12-13 15:23:46
@cloud107202 需要补的是什么基础?大佬给个方向?
kingofzihua 2021-12-13 15:26:09
@ch2 按照我理解,比如需要读取文件,磁盘 IO 会比较慢,系统就会挂起当前线程,然后切换其他线程,你如果用协程,系统调用还是操作系统给你切换到其他线程了,协程的意义在哪里呢?
kingofzihua 2021-12-13 15:27:56
@crazywhalecc 这个东西取决于你要做的事情吧,比如网络是非阻塞的 IO ,这时候你可以等待,然后做其他的事,但是 java 没用协程,NIO 也一样高性能啊。
Juszoe 2021-12-13 15:31:03
简单来说,就是把线程切换的控制权从操作系统转移到应用程序,操作系统切一次线程代价不小
anytk 2021-12-13 15:31:09
1. 协程是用户自己调度,切换开销堪比函数调用,比线程轻量
2. 协程可以对逻辑解耦简化,异步操作“同步”化,比如解决 callback hell
whoosy 2021-12-13 15:31:48
线程进程切换涉及到内核调度,也就是要从用户态切换回内核态,切换有成本,比如虚拟内存、堆栈的切换。
协程不被操作系统感知,协程切换完全由程序控制,切换成本忽略不计。
coldear 2021-12-13 15:40:15
别的语言不清楚,kotlin coroutuine 实际就是 callback ,最终执行的时候,就是线程池在执行不同的 callback ,
一个 coroutine 有一些 local 的状态,只需要一点点内存就可以存储,不需要 stack 之类,比线程要轻量。
多个 coroutine 可以在一个或者几个线程上执行,所以 context switch 要少很多。
不过也没有什么魔法,该 block 的地方还得 block , 只不过 coroutine 内存占用很小,可以创建非常多,一般不用考虑资源限制,这样不用写各种资源管理的代码,让代码看上去更简单。
liyixin95 2021-12-13 15:44:42
rust 移除内置协程的原因是其不符合零开销抽象原则,何为零开销抽象:
1. 如果你不需要这个功能,你就不用为这个功能付出代价
2. 如果你需要这个功能,你没法写出开销更小的代码

协程需要自带一个 runtime ,这对不需要协程的用户来说就违背了第一条原则,故移除掉了。
learningman 2021-12-13 15:46:58
比如说,一次 IO 请求,延迟是 300ms ,但是发出请求 1ms 就够了,这就是典型的协程的应用场景。
不然你应该怎么做?开一个线程,挂起等返回?这是操作系统切换的,太重了,协程轻的多也快的多
kingofzihua 2021-12-13 15:52:17
@learningman 这个例子感觉举得不太好,或者我没理解你说的,java 没有 协程,NIO 一样能高吞吐!并且也没有一次 IO 就挂起。也不需要线程切换,不然 java 早就被淘汰了
documentzhangx66  3 2021-12-13 15:52:59
1.我们只需要 CPU 运行我们自己写的代码,尽可能少的运行别的代码。

2.协程或纤程 = 我们写的代码 + 尽可能少的程序框架的管理代码

3.线程 = 协程或纤程 + 操作系统的一小部分管理代码

4.进程 = 线程 + 操作系统的一大部分管理代码

如果这样说,你还不明白,再举个例子:

协程或纤程 = 你自己想喝水,你步行 2 分钟,花 2 元买了一瓶农夫山泉。

线程 = 你叫了美团,花了 3 元农夫山泉 + 1 元配送分 + 等候 20 分钟。

进程 = 你上淘宝,花了 3 元农夫山泉 + 10 元快递费 + 等候 3 天 + 花了 10 分钟走路去菜鸟驿站取回。
Edsie 2021-12-13 15:58:41
楼主了解下线程切换的开销问题

#4 kingofzihua 我一个线程内为啥要进行多个协程切换?
如果你只有一个线程的话,那就不是多线程问题了

还有,NIO 高吞吐实现,是基于 IO 多路复用,跟这个不是一回事。
hguandl 2021-12-13 15:59:36
你自己也说了“如果不是系统阻塞,那我完全可以在线程内执行其他的,后面再回来执行”。其实协程就是对你这种做法的抽象,通过生命周期和上下文来更轻松地控制调度。这和你直接在线程内做非阻塞 IO 没有本质区别。协程的思想可以降低写非阻塞程序时的心智负担,提高可读性、降低出错的可能。
fkdtz  2 2021-12-13 16:02:04
首先提这样一个问题:“多线程已经能够实现并行执行了,为什么还要协程?”。

用一句话解释就是协程的效率比线程更高。

因为协程是在用户态模拟操作系统对线程调度的过程,协程编程中常见的关键字 yield ,在操作系统中也有原型:yield() 函数。

这就省去了操作系统在上下文切换时产生的额外的寄存器的读写,充分利用单核 CPU 计算能力。

所以想要理解协程,可以参考操作系统对 CPU 的虚拟化和对线程的调度过程,尤其是当发生中断时(如发生 IO 事件时),操作系统如何调度,协程与之十分相似。
ipwx  2 2021-12-13 16:03:24
“如果阻塞是系统调用阻塞,线程就会挂起,调度到其他的线程了,你协程没用”

所以协程要配合非阻塞 IO 。

原先 callback 或者 Future.map 就是非阻塞的,但是写起来心智负担太大。所以抽象了一种 IO ,形式上是阻塞的,但是实质上是非阻塞的。await non-blocking read 会把控制流立刻转向其他协程,而当这个 non-blocking read 成功以后这个协程会重新进入调度队列。
MakHoCheung  1 2021-12-13 16:06:46
我来通俗发表一下我的观点,协程其实就是并发编程的一种语法糖,它可以让线程执行到某行代码(比如 HTTP 请求)后停下来不再继续执行下面处理返回的代码,反而去跑其他的代码。为什么要这样做呢,因为可以让线程无时无刻在运行代码,不会因为等待 IO 阻塞而空闲着,比如 IO 多路复用就可以循环查询 IO 事件,没有查到就去执行队列的任务。如果不用协程的话,完全可以基于 NIO 的 IO 多路复用实现 EventLoop 充分利用线程去工作。只是 Java 没有这种语法糖而已。简单一句就是以前 IO 读阻塞、写阻塞等阻塞导致线程挂起空闲,现在可以让线程在遇到读阻塞和写阻塞的时候去执行其他任务。
ipwx  5 2021-12-13 16:08:47
并发种类:

1. 多线程:太慢

2. callback:代表作为 Node.js 、python tornado ,boost asio 。但是会陷入 callback 地狱。

3. Promise / Future:java, scala, js, 比 callback 好多了,目前是主流技术之一。缺点是要仔细管理闭包的嵌套。

4. event loop:一般 c/c++ libev libuv ,还有 python gevent 。心智负担比上述三种都大,但是可以更精细操作、更高效。底层实现一般为 kqueue 和 linux 上的 epoll ,或者 fallback 到 select 。

大名鼎鼎 nginx 就靠 event loop 暴打同时代。

5. 协程。

协程一般用 event loop 实现,这种协程就是对 event loop 的抽象。要理解协程,建议稍微学习一下 event loop 。
ipwx 2021-12-13 16:10:07
其实这里有两条技术路线

callback => promise / future
event loop => coroutine

要理解协程就要去理解这两条技术路线的区别。
momocraft 2021-12-13 16:10:13
> 如果不是系统阻塞,那我完全可以在线程内执行其他的,后面再回来执行
这个听起来就挺像协程
0o0o0o0 2021-12-13 16:10:23
@ipwx 补一个 async/await ,不过实现方法有很多
MakHoCheung 2021-12-13 16:10:41
普通的 Java 利用到线程池的并发编程都可以做到协程的性能,只是写起来后者更轻松
cloverzrg2 2021-12-13 16:11:16
CPU 密集型:用线程,线程数为 cpu 核心数
IO 密集型:用协程,一个请求起一个协程
ch2 2021-12-13 16:13:31
@kingofzihua #17 "你如果用协程,系统调用还是操作系统给你切换到其他线程了",非阻塞 IO 模式下线程可以批量获取一大堆文件 /网络套接字的读取结果,协程可以按阻塞 IO 的编程方式写出来非阻塞 IO 模式下不需要频繁切换 CPU 的代码的性能
MakHoCheung 2021-12-13 16:16:17
“阻塞是系统调用阻塞,线程就会挂起”,重点就这里了,阻塞的时候线程不会挂起(因为 NIO 、IO 多路复用、EventLoop )这可以解答到你的疑惑了
oxromantic 2021-12-13 16:16:49
协程一般配套的就是无阻塞模型,再加上没有线程切换,可以最大化压榨性能,从语法上给你同步的体验,性能是 callback 的性能
ipwx 2021-12-13 16:16:55
@0o0o0o0 其实感觉 async/await 就是协程的一种(个人认为最优美的)表现形式。然这需要大量语法糖支持。

Python 和 JS 的 await 都已经上线了,用起来其实挺爽的。只是 Python 方面的支持库力度还不行。

C++20 的 coroutine 虽然语法糖标准已经发布了,但是根本没有支持的库(差评)。还得等几年。
tpkeeper 2021-12-13 16:18:34
缩小了资源调度的粒度
murphytalk  1 2021-12-13 16:20:48
@kingofzihua 异步的 lib 或 API ,传统做法是用回调函数,问题是你要同时协调多个异步调用甚至处理异步调用结果的回调函数里还有其他异步调用的话这些回调函数就会显得凌乱,所以被叫 callback hell 。另外有人对这类被异步调用的回调函数的执行顺序脑子转不过来,协程不增加线程上下文切换的开销,但是能让你以“传统”方式组织逻辑。
不过话说回来,我的一个项目我选的是 vertx 和 kotlin 。vertx 你大概知道所有 API 都是异步的,但 Kotlin 虽有协程但我觉得借助 Vertx 的 Future 和 Promise ,我同样可以把异步逻辑组织得很清爽,我就根本没用 kotlin 的协程。
xx6412223 2021-12-13 16:22:58
大多的 IO 密集型任务并不需要独立的线程,而只需要的仅仅是异步
wanguorui123 2021-12-13 16:26:24
协程就是用户态的线程,以前线程是操作系统来调度,操作系统的线程调度开销大,而现在协程由用户态上的队列协调器来调度。
可以用少量的线程通过线程里的协调器维护更多的协程,线程少了操作系统内核的调度压力就小了,用户态的协调器是跑在线程上,当遇到异步 IO ,以及协调器提供的伪阻塞这些方法后这个协程就会挂起,这个挂起是轻量级别的,等待下次被唤醒。

有了协程后就可以创建很少的线程,线程间的切换就少了许多,更多的切换是线程内跑的队列协调器,由协调器控制每个协程的状态。
kingofzihua 2021-12-13 16:26:30
@ipwx 所以说协程就是一个封装的方法问题? 如果遇到阻塞 IO ,协程就没用了?
目前我只知道网络是非阻塞 IO , 并且这个是操作系统支持的,java 高性能的 网络框架性能也很好的点在于 网络非阻塞 IO ,放到任何语言里都是。
我所说的协程其实根本和性能无关? 我应该吧协程和 callback 或者 event loop(目前还不太清楚是啥) 等 解决并发的方式来比较对吗?
crazywhalecc 2021-12-13 16:28:29
@kingofzihua 高性能有不止一种方案实现,协程是其中一种。但协程我个人觉得比较突出的特点就是可以用传统同步代码的逻辑来编写异步才能实现的过程。
wanguorui123 2021-12-13 16:30:29
@wanguorui123 JAVA 虽然没有原生的协程,但是 JAVA 有线程池和消息队列,这样也可以用少量的线程维护大量的消息。
ipwx 2021-12-13 16:34:21
@kingofzihua

1. 对,协程只对非阻塞 IO 起作用。
2. 但其实,所有上述的并发方法都是针对阻塞 IO 减少开销的。对于 CPU 密集型任务,如果只有 2 个核,同时跑 4 个矩阵计算是没有意义的。
3. 在实践中,通常把 IO 任务和计算任务分开。IO 靠比如 Promise/Future ,event loop ,协程搞。如果需要调用计算任务,就扔到一个固定线程数的线程池上跑,然后协程进入 await 。等计算完成 await 被唤醒,再继续做后面的 IO 。
wanguorui123 2021-12-13 16:34:56
@wanguorui123 协程的好处就是 js 中的 asyc await ,实现伪同步代码
retamia 2021-12-13 16:35:59
协程就是程序自己去实现内核多线程的保存和切换当前上下文环境。
服务端程序大部分都是 IO 密集型,协程只使用一个 CPU 核就可以跑到很高的处理能力了。

如果你想看实现原理,可以看下 setjmp 和 longjmp 。如果想了解概念和理论,可以参考 LISP 里面的 continuation ,call/cc 概念。
Leviathann 2021-12-13 16:37:04
rust 没有 但是语言层面定义好了接口
kotlin 也是 coroutine 是 kotlinx 下面的库不是 kotlin 语言里的东西
语言里有 suspend 关键字
ipwx  1 2021-12-13 16:37:29
通常一个应用程序服务器可以划分为两部分线程组:

1. IO 线程,处理网络 IO 、数据库客户端、等待后台任务、(轻量计算)汇总各种结果。
2. 计算线程,应当是固定数量的线程组成的池,这样一个计算任务丢进来可以排队。

也可以是分离的进程乃至于不同机器处理这些不同的事情。如果是不同进程,可能就会上 zmq 之类的 ipc message queue 。如果是不同机器,那么就会上 rpc 、http api 、redis 或者更重量级的 mq 。
anytk  1 2021-12-13 16:41:51
@kingofzihua
IO 的模型:
以买餐为例,忽略细节
1. 阻塞:经典模式接口。你到餐馆买餐,在餐馆一直等到做好打包,拿了带回来。
2. 非阻塞:电话订餐,每隔小段时间重新打电话问餐馆做好没(其他时间有自由时间处理其他事情),做好了就去拿回来。这电话就是非阻塞调用,但是依然是操作系统调用,会有内核陷入。
3. 异步:操作系统提供接口支持,也就是 event loop 模式。电话订餐,餐做好了餐馆直接电话给你通知好了,你去取餐。

协程用在 IO 密集的场景中,通常都是和 event loop 配合,简化复杂的 callback 序列。
misaka19000 2021-12-13 16:51:02
因为线程切换要操作系统做,需要很多操作,从用户层到内核层要进行大量的切换,比较慢;此外线程因为需要保存的变量比较多,涉及到的地方比较多,所以创建的越多耗费的资源越多,导致操作系统不能够创建太多的线程,一般是几千级别。

协程切换需要做的操作比较少,比较快;而且因为协程涉及的资源少,所以可以大量创建,一般可以是十万级。
Orlion 2021-12-13 16:52:17
1. 协程解决的问题
java 实现一个支持并发的 http server,最简单的方案可能就是拿到一个连接就创建一个线程来执行,即一个多线程的 server 。这个 server 连接数高了性能就会非常差,因为线程数会很高,线程切换开销比较大。

现在有个协程,因为协程开销比较小,你仍然可以用这个简单的思路去实现,即拿到一个连接就创建一个协程来执行。go 标准库中 http server 就是这么实现的,简单粗暴。在 go 中实际是封装了 epoll(linux 平台),由 runtime 调度协程跑在很少数量的线程上,实际上压根就不是原始的多线程方案了,应该是 epoll 方案,所以对于用户来说能以很简单的方式就能实现高性能并发,这就是协程解决的问题之一。
misaka19000  1 2021-12-13 16:55:16
由于上面所说的操作系统的线程的天然的劣势,所以我们一般避免创建太多的线程,但是在面对 IO 操作的时候尤其是网络 IO ,我们必须要使用大量的线程,所以非阻塞 IO 和多路复用被发明来解决在海量 IO 需要海量线程的问题,使用多路复用+非阻塞 IO 可以保证使用很少的线程就实现大量 IO 读写。

但是多路复用和非阻塞 IO 的问题是什么?问题就是太复杂了,尤其是其中很多的异步操作是反直觉的,导致用起来很麻烦而且代码写起来也是很多异步和回调,代码可维护性很差。协程作为对这些操作的封装,可以让程序员更好的实现海量 IO 的读取操作同时不影响操作系统性能。
misaka19000  1 2021-12-13 16:55:44
可以看我以前写的文章:多路复用、非阻塞、线程与协程

https://www.nosuchfield.com/2019/01/09/Multiplex-and-non-blocking-and-threading-and-coroutine/
kingofzihua 2021-12-13 16:56:00
@anytk 稍微懂了,这些博客害人啊,光说比线程轻量,切换开销小,我一直是协程和线程进行对比,以为和线程调度相关,没想到是和 IO 模型相关,不同的处理方式。要说和 callback 相比的话我就能稍微理解点了
xuhaoyangx  1 2021-12-13 16:56:43
协程是可以由程序自行控制挂起恢复的程序。关键词就是挂起恢复。

用协程和不用协程,我理解的优势是

1 、用来多任务协作执行 2 、异步任务控制流灵活转移方便

更浅显点的是 code 层面

1 、异步打码同步话 2 、降低异步程序设计复杂

关于你说的操作系统调度什么的,和协程不是同一层面的东西。

说句不怎么正确的话,协程是线程操作的上层封装?方便你控制
xFrye 2021-12-13 17:03:41
不考虑协程底层怎么实现的情况下,协程最直观的作用就是把一些需要异步 + callback 的代码以同步的方式表达出来,这样在代码结构上更加直观。同一个线程处理 n 个协程( callback ),切线程总比你这样切 callback 的代价要大
anytk 2021-12-13 17:04:09
@kingofzihua 协程的另一个用法是用在插件系统开发中,插件部分是一个小的 context ,但是使用又是受限的,可以由主程序提供多个接口,配合主程序的 eventloop 来实现插件的受限自定义开发工作,降低插件开发的难度要求。
kingofzihua 2021-12-13 17:06:51
@misaka19000 好文
codehz  2 2021-12-13 17:08:35
协程就是一种代码的组织结构,你当然可以全部使用手写状态机来做,事实上早期开发大部分都是这个模型。。。
作为一种结构,它可以大幅度简化某些场景的代码复杂度,这就是它最初的意义。
即使传统磁盘 IO 没办法使用 poll 的模型(因为总是 ready ),为了代码上的统一,做成线程池支撑,上层逻辑用协程描述也无可厚非(虽然现在新的 io 模型已经支持提交大量磁盘请求然后等一起完成了)
至于性能方面的提升,那有也只是副作用,即降低了编码复杂度导致可以从更高抽象角度优化业务逻辑,而不是拘泥于底层状态机的实现细节。
这里和同步 /异步,阻塞 /非阻塞其实关系不大,即使是一个纯算法,也可以使用协程来描述,generator 的模式和协程其实就差不多同构(可以互相转换),与其在需要数据时耦合上获取数据的代码,不如做一个 generator 将数据获取的过程抽象出来,generator 可以让你使用传统控制流结构来描述逻辑,同时内部状态就直接用 generator 中的局部变量表示。
PDX 2021-12-13 17:15:51
协程解决了异步代码不能像写同步代码一样简单的问题。

都说什么调度啊,阻塞啊什么的,我觉得这都不是关键。关键是考虑一个程序员写代码的体验。在兼顾性能的情况下,安全且代码可读性强。
lbp0200 2021-12-13 17:17:23
先创建一台 Linux 虚拟机,单核 1G 内存
yaphets666 2021-12-13 17:20:37
看下来这玩意和 js 的 async await 很像啊
fpure 2021-12-13 17:25:52
@yaphets666 async/await 就是协程啊
ming159  1 2021-12-13 17:28:29
重要的事情 吼三遍
协程的目的不是提升性能! (当然比纯线程还是有提升的)
协程的目的不是提升性能!
协程的目的不是提升性能!

协程是简化了多线程开发难度!
协程是简化了多线程开发难度!
协程是简化了多线程开发难度!


可以用 多线程版完成一个 3 个生产者,2 个消费者程序,再用 Go 的协程+channel 实现一版,就更能体会到了, 更像是一种语法糖. 其意义在于从语法层面降低: 多线程 /异步 开发门槛.
kingofzihua 2021-12-13 17:37:21
@ming159 谢谢,懂了
yulon  1 2021-12-13 17:40:08
协程在最初是轻量级线程,而发展到如今在并发编程中其实是和闭包相对应的概念,只是一个是同步语义一个是异步语义。
有栈协程:轻量级线程,Windows 自带的 Fiber ,POSIX 在没有硬件支持下提供的 Pthread 。
共享栈协程:更轻量的轻量级线程,对栈内存需要小心操作,多用于没有原生支持无栈协程的语言。
无栈协程:异步闭包的语法糖,编译后的底层代码就是异步闭包。
KevinBlandy 2021-12-13 17:41:46
好像 java 的 vertx 事件驱动模型,完全非阻塞。性能方面可以把 Go 的协程吊起来打。
fgwmlhdkkkw 2021-12-13 17:43:31
避免空转
pkoukk 2021-12-13 18:12:11
协程和线程的关系就像水果刀和杀猪刀
你说协程能解决什么线程解决不了的问题么?没有
协程纯粹就是更轻更方便
trcnkq  1 2021-12-13 18:27:48
这玩意老有人的问关键就在于不懂得人也喜欢煞有介事地胡说八道说上两句,这导致每来个新人都需要经过大量的亲身体验才能去伪存真。
除了“协程”,还有__,__和__,你都能有这种体验。
notommorrow  1 2021-12-13 18:50:20
协程是线程的实现之一
操作系统本身支持了线程和线程的调度,各种语言在实现自己的线程时通常会三类:
A. 程序线程 : 操作系统线程 1:1 , 即程序中的一个线程就对应操作系统的一个线程。这样程序就可以把线程的调度完全交给操作系统。但是 1.操作系统的线程数是有限的,因此 Java 可创建的线程数也是有限的,向 tomcat 常见的线程数量是几十个。2. 操作系统调度时,要从用户态切换成内核态,代价高(代价高是相对另外两种方式来说)
Java 之前的主流线程框架就是这么做的。一个典型的列子是,给 Java Thread 设定 Priority 不一定能生效。因为对 Thread 的 Priority 的处理是操作系统在处理。比如 Java 有 10 个 Priority 可能操作系统只有 5 个 Priority 。

B. (协程)程序线程和 :操作系统线程 N:1 , 即程序线程的调度完全由 程序自己处理,不依赖操作系统的线程调度。这样的好处:1. 线程数量不受限制 2. 线程调度不用切换用户态到系统态 。 坏处: 线程调度实现很麻烦。线程状态保存和恢复很复杂,一种常见的折中方案是,协程只支持无状态的函数式调用。

C: 程序线程 :操作系统线程 N:M, A ,B 折中


为什么需要协程: 协程在 java 里很早就被实现过 作为 Greed Thread 的概念。但是使用的不多,很快被废弃了。这些年微服务兴起,一个 web call 通常要数个 micro service 合作才能完成,http 请求数量指数级上升。微服务的特点, http 请求多,单个请求的处理时间短。这时候如果依靠 1:1 的线程实现,由于线程数量有限,当 http 请求数量超过一定数量级时,一个 http 请求在等待和调度上花费的时间甚至会超过任务执行所需的时间。 这时候很多人就希望一种线程方案,他能支持更多的线程个数,调度时间代价更少 -》 协程
kekxv 2021-12-13 19:00:44
有个不太合适的举例:读取文件需要 300ms ,可以切换为 10 份或者 50 份,在中间穿插操作其它业务,可能最后的结果读取文件变成了 310ms 或者 400ms ,但是在这个过程中同时还处理了 n 个任务,虽然增加了读写文件的时间,但是少了线程的开销以及性能的占用,而这个 io 的时长是可接受的
lemonf233 2021-12-13 19:21:52
@trcnkq #75
我猜其中一个空格是 IO 多路复用
aidoudou 2021-12-13 19:24:36
进程:操作系统调度
协程:自己调度
Buges  1 2021-12-13 19:38:54
cooperative multitasking 和 asyncio 和 concurrency 和 parallelism 是不同的事情,楼上不少人都搞混了。
单单回答楼主关于协程比线程轻量在哪里:线程( OS thread )是一种 preemptive multitasking 的实现,线程的切换由外部控制,对代码本身透明,不同线程之间相互争抢,当 cpu 切换时需要保存的状态很多(因为操作系统不知道你具体需要哪些,只能都保存下来)。而协程则是 cooperative 的,即你的代码在不需要时(如等待 IO )主动释放控制权,不同协程之间相互协作,切换时主动指明 resume 时需要的状态( async/await transform 成的 state machine 指的就是这些状态),所以更轻量,相应的代码也需要额外的复杂度,并且需要注意 blocking 问题(没有主动释放控制权)。
宣称 OS thread 开销大是因为内核态用户态切换之类的并不准确,因为操作系统也是可以实现 cooperative multitasking 的,只不过这样的系统上运行的用户程序如果出问题(没有主动释放)会卡住整个系统,所以通用操作系统都是采用的 preemptive multitasking 。
newmlp 2021-12-13 19:49:09
协程就是超级快速在各协程上下文进行切换,类似于 cpu 执行多进程的情况
buffzty 2021-12-13 20:17:31
用协程的最主要原因是让你少写代码,减少心智负担. 协程底层就是一个线程池+runtime+任务队列.
java 的 Future,c++ 的 fiber 跟 go 的协程差不多. 不过 go 的协程功能比他两多点. 会自动控制任务的执行时间,协程内部可以让出执行权. 因为 go 的 runtime 帮你写了一大堆代码. 并且开一个新任务只需要 "go" 一个关键字 ,而 java 需要用一个函数包住它.要多写几个字符. go 的协程出名还有一个重要的点就是 语言层支持 chan 通信. 如果你用 java c++都可以实现 go 的 csp 编程, 但是会比 go 多写一些代码 会让你的代码不 java
letking  1 2021-12-13 20:24:32
如上面所说,线程和协程核心的区别就是处理 IO 阻塞时的不同。
操作系统实现的线程调度是一种通用算法,因为操作系统是无法知道你的线程内部究竟在做什么,它只能分时复用隔一段时间来调度一下每个线程,即使某些线程还处于 IO 阻塞状态,操作系统也得把这个线程切换到 CPU 上去执行一下子。
而协程是自己调度的,它不是“切换开销小”而是基本没有所谓的切换开销,因为你自己是完全知道代码何时会进入阻塞、何时会恢复的,发生阻塞时可以安心去执行其他协程,根本无需在中途切换回来看 IO 是否已经就绪。

总的来说多线程就是你交给 CPU 很多个各式各样的任务,CPU 为了保证这些任务都能正常执行,不得不给每个任务分出一个时间片间隔执行,保证所有任务都在运行。多协程是你把多个任务的 DAG 图都画好了交给 CPU ,CPU 只需要按照你的图按部就班地线性的执行即可。
shew2356 2021-12-13 21:04:31
Java 是支持协程的~
wyx119911 2021-12-13 21:11:20
通过没有原生协程语言的协程化改造,可以更好地理解协程的意义。
如果是重 io 轻计算的场景,使用 io 多路复用进行事件回调( NIO ,epllo )优化并发量比粗暴开线程更省资源,效果更佳。但是这意味着极大提高 io 编程时的工作量和心智负担。
一个 while(true) {
read();
}
wyx119911 2021-12-13 21:18:22
通过没有原生协程语言的协程化改造,可以更好地理解协程的意义。
如果是重 io 轻计算的场景,使用 io 多路复用进行事件回调( NIO ,epoll )优化并发量比粗暴开线程更省资源,效果更佳。但是这意味着极大提高 io 编程时的工作量和心智负担。
所以一个 while(true) { read(); } 要改造就要编写维护大量 epoll 事件。
c++在没有原生协程支持的情况下,引入 libco 这样的库,可以不改动代码的情况下异步协程化。做法是 hook 这个同步 read 函数,在里面添加、回调 epoll 。使用方无感知,降低了开发复杂度,这就是协程的优点。
qingtengmuniao 2021-12-13 21:47:47
协程,本质上是用户态线程。其好处在于占用资源更少,上下文切换代价更小。对于计算密集型应用来说,可以轻松开到十万级以上并发。
线程、协程本质是什么?是一个逻辑上的顺序执行流,这种模型能极大降低编程难度。因此,尽管可以通过在线程内做异步编程、IO 多路复用等等策略来充分利用 CPU ,但他们会让用户代码变得复杂而难读。
总结来说,协程使得用户可以编写同步代码,来充分利用 CPU 。而将协程上下文保存、协程调度等细节交给协程库去处理。
BigDogWang 2021-12-13 22:04:58
楼上说的很清楚了,补充一点,IO 接口不一定会一定阻塞。系统 IO 接口有一种是非阻塞 IO 接口,并不会挂起当前线程。甚至于我猜测协程就是基于非阻塞 IO 接口和消息队列的
BigDogWang 2021-12-13 22:06:29
@BigDogWang 你也可以不用协程自己有非阻塞 IO 和消息队列搞一套类似的。问题的关键在于没有语言层面的支持,你会写出一堆回调的
NeroKamin 2021-12-13 22:57:31
协程最大的作用还是降低编码心智负担
wyx119911 2021-12-13 23:30:23
@qingtengmuniao “对于计算密集型应用来说,可以轻松开到十万级以上并发”这句是不是打错了?
FrankAdler 2021-12-14 00:16:05
看了上面的很多回答,真不喜欢程序员举例子
kisick 2021-12-14 00:56:19
@qingtengmuniao 计算密集型的场景,为什么要开协程,看了上面的回答,协程不是用于非阻塞 IO 时吗?
limbo0 2021-12-14 01:07:43
看了楼里, 觉得自己理解太肤浅了
1423  3 2021-12-14 01:27:05
感觉全部人都在空谈。。没有实际经验

就我而言,协程颠覆了网络编程
在协程出现前,网络编程库( C/C++)用 epoll 那一套,库对外提供的接口往往是回调接口;就是当某事发生时,库会调用你指定的函数来处理事件。

毫无疑问 epoll 的使用和回调的管理是比较难处理的,这也是网络编程成为一个“技能点”的原因

协程广泛使用后,C/C++ 出现了基于协程的网络库,一般 hook 了 read write 等系统调用,使得调用者不再需要关心控制流切换,更不用使用异步回调的方式进行网络编程。

但 C/C++ 库无法彻底避免用户对会造成阻塞的网络 api 的调用。一旦发生就是灾难。

而 golang 的出现解决了这一问题,golang 是语言级别的协程,彻底的解决了“网络编程是困难的”这一问题,并且几乎是网络编程的最佳实践。

golang 对网络编程的贡献使得无数人可能引以为傲的 C/C++ 网络编程技能变得不再重要,因为用 go 实现工业级网络协议处理的门槛大大降低了。
1423 2021-12-14 01:32:15
协程是很早就有了的技术,之所以最近几年被大众瞩目,就是因为 golang 用了协程。
协程对控制流的管理不是新鲜事物,C/C++ 等语言实现的协程机制也不是新鲜事物。
协程被 go 广泛引入到公司级的网络程序中,才是新鲜事物。

一家之言,仅供参考,欢迎批评
ysc3839 2021-12-14 01:41:10
我个人不是很懂协程,只是稍微看过一点无栈协程的资料,感觉上有栈协程和无栈协程还是有些区别的,建议学习时分开研究。
个人觉得无栈协程更像是回调函数的语法糖,不必须要调度器 (但是要配合调度器用也是可以的),可以很容易跟一些使用回调函数的场景对接。个人建议看看 C++20 的 coroutine 和 JavaScript 的 async await 。Python 的 async function 比较特殊,说是无栈协程,但是捆绑了调度器,不能像回调函数那样使用。
至于有栈协程我就不太了解了,感觉上比无栈协程复杂。
2i2Re2PLMaDnghL  1 2021-12-14 01:41:17
实际上就是『在线程内执行其他的』,只是我们将其中一种方法取了个名字叫『协程』
用 stackful 的形式去描述在一个线程内的多项计算,在文法和语义界面上就呈现出一种『活动的栈的切换』(实际上是否发生栈切换视具体实现而定)。而这种活动的栈的切换看上去又和线程切换类似,所以就认为它是一种更轻量的线程。

话说回来,这么多楼竟然没有一个人谈到 Actor 模型的。

也只有一个人谈到 call/cc ;实际上,你会发现 lisp 语言的大部分方言根本不用进行任何改变就支持协程,你大可以自己实现,Wikipedia 的列表里将其称为 trivial 的。
当然,Coq 里你甚至可以自行实现一个 call/cc ,但它的执行结构似乎不能正确地设计出协程()
ysc3839 2021-12-14 01:52:03
@1423 我觉得要分开来看。有栈协程好像上世纪 90 年代就已经出现了,最近才随着 Golang 热门起来。
另一边无栈协程或者说 async function/generator 则是近十年随着 C#, JavaScript, C++ 逐渐发展起来。
zhang2e 2021-12-14 07:36:49
说说我了解的 Kotlin 在 Android 中实现的协程吧( Kotlin Jvm 中的协程的实现是另一种),本质上目的就是帮你切换线程,你不直接操作线程,你也无需了解线程的知识,但你可以说他就是线程。

那为什么要发明这种东西呢,做过 Android 开发都知道,线程切换是很频繁的事情,需要在子线程中异步处理耗时任务,然后还要切换回主线程中处理 UI 操作。

Android 原生给的方案是 Handler 和 Looper ,这样写就是不可避免遇到回调地狱和线程管理中不恰当创建切换线程带来的性能损耗问题。而协程把上面的脏活累活都给你干了。

之前的 RxJava ,也是解决这个问题,不过协程可以像写同步一样写异步,更符合直觉,更可读。