Go-协程机制

目录

一、线程 VS 协程

1、什么是线程,什么是协程。

2、创建时默认栈(stack)的大小比较

3、和内核调度实体 KSE 的对应关系比较

a、1:1

b、1:N

二、Go 的调度机制

三、Go协程的使用


一、线程 VS 协程

1、什么是线程,什么是协程。

       在理解Go的协程机制之前,我们需要先理解线程和协程的区别。表面上看协程和线程似乎是同一个东西,能达到的效果也相同,但是在底层的实现上却有着非常大的区别,在服务器端的绝大部分应用中,协程要比线程节省资源的多。

通俗易懂的讲,线程是操作系统的资源,当java程序创建一个线程,虚拟机会向操作系统请求创建一个线程,虚拟机本身没有能力创建线程。而线程又是昂贵的系统资源,创建、切换、停止等线程属性都是重量级的系统操作,非常消耗资源,所以在java程序中每创建一个线程都需要经过深思熟虑的思考,否则很容易把系统资源消耗殆尽。

       而协程,看起来和线程差不多,但创建一个协程却不用调用操作系统的功能,编程语言自身就能完成这项操作,所以协程也被称作用户态线程。我们知道无论是java还是go程序,都拥有一个主线程,这个线程不用显示编码创建,程序启动时默认就会创建。协程是可以跑在这种线程上的,你可以创建多个协程,这些协程跑在主线程上,它们和线程的关系是一对多。如果你要创建一个线程,那么你必须进行操作系统调用,创建的线程和主线程是同一种东西。显然,协程比线程要轻量的多

        既然协程这么优秀,为什么不彻底替代线程呢?事实上协程和线程完全不是两个相同层面的东西,完全谈不上替代一说,协程可以说是一个独立于线程的功能,它是在线程的基础上,针对某些应用场景进一步发展出来的功能。我们知道,线程在多核的环境下是能做到真正意义上的并行执行的,注意,是并行,不是并发,而协程是为并发而生的。

       打个简单的比方吧,射雕英雄传看过吧,周伯通教郭靖一手画圆,一手画方,两只手同时操作,左右互搏,这个就是并行。普通人肯定做不到,不信你试试。你不能并行,却可以并发,你先左手画一笔,然后右手画一笔,同一时候只有一只手在操作,来回交替,直到完成两个图案是,这就是并发,协程主要的功能。

我们画一个示意图如下:

       想象一下业务场景,你需要执行两个互不依赖的sql查询,为了减少等待时间,常规的操作肯定主线程执行sqlB的同时另起一个线程执行sqlA,使两个sql并行执行。然而你会发现,执行两个sql的线程大多数时间只是在等待数据库服务器的响应,线程只是处于阻塞等待状态,而不是疯狂运转,而线程的创建、切换又很消耗系统资源,显然这很浪费。这个时候就该协程大展身手了,你可以在主线程中创建一个协程用于执行sqlB,然后再在主线程中执行sqlA,协程和线程一样,不会阻塞主线程,所以sqlB得到结果后,你可以通过语言的api去看看在协程中的sql执行完毕了没有,如果没有则等待,如果执行完毕了就拿结果,和线程操作几乎一摸一样。至于sqlA和sqlB是否真正在并行执行根本无所谓。为什么呢? 我们假设执行一个sql需要三步,提交sql、等待、获得结果 ,其中第一步和第三步极省时,只要1毫秒一步,而第二步却要1000毫秒,那么使用并行的多线程执行两个sql,你只要花掉1002毫秒,而使用并发的协程你要花掉1004毫秒,但是线程比协程多消耗一个线程的资源,请问你会为了这2毫秒而选择多线程吗,显然不可能,创建线程的开销都要大于节省下来的时间,这就是协程存在的理由

preview

       而服务器端开发中,大多数时候都是要花大量等待时间的场景,也就是所谓的IO密集,协程极为适合这种场景,而go又主打协程,直接从语法层面支持,切中了以往开发高性能程序太过于复杂的痛点,因此广受程序员们的欢迎。java其实也可以模拟出协程的效果,比如用nio和多线程,也能假装goroutines的效果,但实际操作起来太过于麻烦,还要掌握一大堆枯涩的概念,完全没有goroutines的优雅。所以在并发性能上,go完胜java。换言之,go比java更适应高并发场景,能更优雅方便的写出高并发程序。

:上面这部分内容是参看了知乎博主的经典回答,略有改动,原文链接:https://zhuanlan.zhihu.com/p/169426477

2、创建时默认栈(stack)的大小比较

  • JDK5以后,Java Thread Stack 默认为 1M。
  • Groutine 的 Stack 初始化大小为 2K。

相比之下,协程的栈大小就小很多了,创建起来也会更快,也更节省系统资源。

3、和内核调度实体 KSE 的对应关系比较

KSE (Kernel Scheduling Entity)

  • Java Thread 是 1 : 1
  • Groutine 是多对多 M:N

那么这种多对多的对应关系对我们程序有什么意义呢?

       如果是1:1,那么我们的线程(thread)是由我们的内核实体直接进行调度,这种方式,它的调度效率非常高,但是这里有一个问题,如果线程之间发生切换时,会牵扯到内核对象的相互切换,这将是一个消耗非常大的事儿。

      相对来说如果是多个协程由同一个内核实体来调度,那么协程之间的切换不涉及内核对象间的切换,在内部就能完成,它们之间的切换就会小非常多了,Go 也就是主打这个方面。

a、1:1

b、1:N

二、Go 的调度机制

Go的协程处理器(Processor)是挂在系统线程(System thread)下面的,协程处理器下面又挂有准备运行的协程队列(Goroutine),每个协程队列中每次有一个协程是是正在运行中的。

如果正在运行的协程执行时间特别长,会不会堵塞注协程队列呢?

Go的处理机制是这样的,Go 在允许协程时,会启动一个守护线程去计数,计每个 Processor 完成的协程数量,当它发现一段时间后某个 Processor 完成的协程数量没有任何变化后,它就会往协程的任务栈里面插入一个特殊的标记,当协程允许遇到非内联函数时就会读到这个标记,就会把自己中断下来,插入到协程队列的队尾,然后切换到下一个协程继续运行。

另个并发机制是这样的,当某个系统线程被系统中断了,例如I/O需要等待的时候,为了提高整体的并发,Processor会拖家带口的移到另一个系统线程当中,继续执行它所挂的协程队列里面的协程。当上次中断的线程又被重新唤醒后,它又会拖家带口的回来。中断期间,协程的允许状态,会保存在协程对象里,当协程再次有允许机会的时候,这些数据优惠重新写入寄存器,然后继续运行。

三、Go协程的使用

Go协程的使用很简单,只需要在 方法前面加一个 go 关键字即可,目前我的测试结果为,只能用匿名函数的方式进行调用,不能在 go 关键字后面跟一个 package 里面的方法。

package goroutine

import (
	"fmt"
	"testing"
)

//Go 协程的使用
func TestGoroutine(t *testing.T) {
	for i := 0; i < 5; i++ {
		//Go协程的使用很简单,只需要在 方法前面加一个 go 关键字即可。
		go func(num int) {
			fmt.Println(num)
		}(i)
	}
}

:这篇博文是我学习中的总结,如有转载请注明出处:

https://blog.csdn.net/DaiChuanrong/article/details/118250000

上一篇Go-依赖管理

下一篇Go-共享内存并发机制

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值