理解Goroutine

Go语言里面的并发使用的是Goroutine,Goroutine可以看做一种轻量级的线程,或者叫用户级线程。与Java的Thread很像,用法很简单:

go
fun(params);

相当于Java的

new
Thread(someRunnable).start();

虽然类似,但是Goroutine与Java Thread有着很大的区别。

Java里的Thread使用的是线程模型的一对一模型,每一个用户线程都对应着一个内核级线程。

上图有两个CPU,然后有4个Java thread,每个Java thread其实就是一个内核级线程,由内核级线程调度器进行调度,轮流使用两个CPU。内核级线程调度器具有绝对的权力,所以把它放到了下面。内核级线程调度器使用公平的算法让四个线程使用两个CPU。

Go的Goroutine是用户级的线程。同样是4个Goroutine,可能只对应了两个内核级线程。Goroutine调度器把4个Goroutine分配到两个内核级线程上,而这两个内核级线程对CPU的使用由内核线程调度器来分配。

与内核级线程调度器相比,Goroutine的调度器与Goroutine是平等的,所以把它和Goroutine放到了同一个层次。调度器与被调度者权力相同,那被调度者就可以不听话了。一个Goroutine如果占据了CPU就是不放手,调度器也拿它没办法。

同样是下面一段代码:

void run() {
  int a = 1;
  while(1==1) {
   a = 1;
  }
}

在Java里,如果起多个这样的线程,它们可以平等的使用CPU。但是在Go里面,如果起多个这样的Goroutine,在启动的内核级线程个数一定情况下(通常与CPU个数相等),那么最先启动的Goroutine会一直占据CPU,其它的Goroutine会starve,饿死,因为它不能主动放弃CPU,不配合别人工作。说到配合工作,那就需要说一下协程(coroutine,可以当做cooperative
routine),协程需要相互合作,互相协助,才能正常工作,所以叫做协程。

协程并不需要一个调度器,它是完全靠互相之间协调来工作的。协程的定义在学术上很抽象,目前实际应用中,协程通常是使用单个内核级线程,用来把异步编程中使用的难懂的callback方式改成看上去像同步编程的样子。

比如nodejs是异步单线程事件驱动的,在一段代码中如果有多次异步操作,比如先调用一个支付系统,得到结果后再更新数据库,那么可能需要嵌套使用callback。pay函数是一个调用支付系统的操作,异步发出请求后就返回,然后等支付完成的事件后触发第一个回调函数,这个函数是更新数据库,又是一个异步操作,等这个异步操作完成后,再次触发返回更新结果的回调函数。 这里只有两个异步操作,如果多的话,有可能会有很多嵌套。

pay(amount, callback(payamount) {
 update(payamount, callback(result) {
   return result;
})});

而使用协程,可以看上去像是同步操作

pay(amount){
  //异步,立刻返回
  //payamount需要操作完成后才能被赋值
 payamount = dopay(amount);
 yeild main;//把控制权返回主routine
 //dopay事件完成后,主routine会调起这个routine,
 //继续执行doupdate
 result=doupdate(payamount); 
 yeild main;  //再次把控制权返回主routine
 return result;
}

(以上都是伪代码)

把原来的各种嵌套callback改成协程,那么逻辑就会清晰很多。

Goroutine与Coroutine不一样,开发者并不需要关心Goroutine如何被调起,如何放弃控制权,而是交给Goroutine调度器来管理。开发者不用关心,但是Go语言的编译器会替你把工作做了,因为Goroutine必须主动交出控制权才能由调度器统一管理。首先我们可以认为写上面那种死循环而且不调用任何其他函数的Goroutine是没意义的,如果真在实际应用中写出这样的代码,那开发者不是一个合格的程序员。一个Goroutine总会调用其他函数的,一种调用是开发者自己写的函数,一种是Go语言提供的API。那编译器以及这些API就可以做文章了。

比如

void run() {
  int a = 0;
  int b = 1;
  a = b * 2;
  for(int i = 0; i < 100; i++) {
    a = func1(a);
 }
}

那么编译器可能会在调用其他函数的地方偷偷加上几条语句,比如:

void run() {
  int a = 0;
  int b = 1;
  a = b * 2;
  for(int i = 0; i < 100; i++) {
   //进入调度器,或者以一定概率进入调度器
   schedule();  
   a = func1(a);
  }
}

再比如

void run() {
  socket = new socket();
 while(buffer = socker.read()) {
  deal(buffer);
 }
}

socker.read()是Go语言提供的一个系统函数,那么Go语言可能在这里面加点操作,读完数据后,进入调度器,让调度器决定这个Goroutine是否继续跑。

下面这段Go语言代码,把内核级线程设成2个,那么主线程会饿死,而在func1里加一个sleep就可以了,这样func1才有机会放弃控制权。

当然Go语言的调度器要比这复杂的多。Goroutine与协程还是有区别的,实现原理是一样的,但是Goroutine的目的是为了实现并发,在Go语言里,开发者不能创建内核级线程,只能创建Goroutine,而协程的目的如上面所示,目前比较常见的用途就是上面这个。Go语言适合编写高并发的应用,因为创建一个Goroutine的代价很低,而且Goroutine切换上下文开销也很低,与创建内核级线程相比,Goroutine的开销可能只是几十分之一甚至几百分之一,而且它不占内核空间,每个内核级线程都会占很大的内核空间,能创建的线程数最多也就几千个,而Goroutine可以很轻松的创建上万个。

Goroutine底层的实现,在Linux上面是用makecontext,swapcontext,getcontext,setcontext这几个函数实现的,这几个系统调用可以实现用户空间线程上下文的保存和切换。

时间: 2024-11-01 19:43:50

理解Goroutine的相关文章

golang技术随笔(二)理解goroutine

进程.线程和协程 要理解什么是goroutine,我们先来看看进程.线程以及协程它们之间的区别,这能帮助我们更好的理解goroutine. 进程:分配完整独立的地址空间,拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程的切换只发生在内核态,由操作系统调度. 线程:和其它本进程的线程共享地址空间,拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度(标准线程是的). 协程:和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制. 进程和线程的切换主要依赖于时

Goroutine是如何工作的

转自:http://tonybai.com/2014/11/15/how-goroutines-work/ 在golangweekly的第36期Go Newsletter中我发现一篇短文"How Goroutines Work" ,其作者在参考了诸多资料后,简短概要地总结了一下 Goroutine的工作原理,感觉十分适合刚入门的Gophers(深入理解Goroutine调度的话,可以参考Daniel Morsing的"The Go scheduler" ).这里粗译

go语言--goroutine

一.goroutine goroutine就是Go语言提供的一种用户态线程.Go自己实现了goroutine的调度器(Scheduler),Go的调度器由三部分组成: M:指的是Machine,一个M直接关联了一个内核线程. P:指的processer,代表M所需的上下文环境,也是处理用户级代码逻辑的处理器.P的数量可以通过GOMAXPROCS()来设置,默认值是CPU的核数. G:指的是goroutine,其实本质上也是一种轻量级的线程. 二.goroutine和线程的区别 1.内存占用 创建

Golang后台开发初体验

转自:http://blog.csdn.net/cszhouwei/article/details/37740277 补充反馈 slice 既然聊到slice,就不得不提它的近亲array,这里不太想提值类型和引用类型的概念(个人觉得其实都是值类型),golang的array其实可以假想为C的struct类型,只是struct通过变量名来访问成员(如xxx.yyy),而array通过下标来访问成员(如xxx[3]),具体内存布局如下图所示: 图 1 golang的array内存布局 显然gola

Golang网络库中socket阻塞调度源码剖析

本文分析了Golang的socket文件描述符和goroutine阻塞调度的原理.代码中大部分是Go代码,小部分是汇编代码.完整理解本文需要Go语言知识,并且用Golang写过网络程序.更重要的是,需要提前理解goroutine的调度原理. 1. TCP的连接对象: 连接对象: 在net.go中有一个名为Conn的接口,提供了对于连接的读写和其他操作: type Conn interface { Read(b []byte) (n int, err error) Write(b []byte)

【Go语言】【16】GO语言的并发

在写该文之前一直犹豫,是把Go的并发写的面面俱到显得高大尚一些,还是简洁易懂一些?今天看到一个新员工在学习Java,突然间想起第一次接触Java的并发时,被作者搞了一个云里雾里,直到现在还有阴影,所以决定本文从简.哈哈,说笑了,言归正传. Go的并发真的很简单,所以本文不罗嗦进程.线程.协程.信号量.锁.调度.时间片等乱七八糟的东西,因为这些不影响您理解Go的并发.先看一个小例子: package main import "fmt" func Add(i, j int) { sum :

从汇编层面看函数调用的实现原理

本文是<go调度器源代码情景分析>系列 第一章 预备知识的第6小节. 前面几节我们介绍了CPU寄存器.内存.汇编指令以及栈等基础知识,为了达到融会贯通加深理解的目的,这一节我们来综合运用一下前面所学的这些知识,看看函数的执行和调用过程. 本节我们需要重点关注的问题有: CPU是如何从调用者跳转到被调用函数执行的? 参数是如何从调用者传递给被调用函数的? 函数局部变量所占内存是怎么在栈上分配的? 返回值是如何从被调用函数返回给调用者的? 函数执行完成之后又需要做哪些清理工作? 解决了这些问题,我

golang语言并发与并行——goroutine和channel的详细理解(一)

如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. 以下是我入门的学习笔记. Go语言的goroutines.信道和死锁 goroutine Go语言中有个概念叫做goroutine, 这类似我们熟知的线程,但是更轻. 以下的程序,我们串行地去执行两次loop函数: func loop() { for i := 0; i < 10; i++ { f

对go的goroutine理解

1.实际是go运行时自己控制线程数目(线程池),执行许多的task(goroutine),防止线程切换的开销,充分利用多核做并行计算 2.在io等需要等待的操作发生的时候go运行时切换task,但是线程不切换继续执行其他task,io完成后挂起的task重新参与调度 3.底层处理io的实现在windows下是iocp,在linux下是epoll,mac下是kqueue, golang自己封装了抽象层(这些都是这些操作系统下目前最优方案). 背景知识自行脑补 1.线程切换的开销 2.epoll 3