golang|golang中并发、gorutine

我们知道golang的一个重要特性就是能够支持极高的并发。而实现这个特性则是golang中的rorutine机制。在说goroutine之前,我们先说明几个概念:进程、线程、协程
进程:程序运行的基本单位,一个运行的程序就是一个进程,进程之间相互隔离,拥有不同的内存空间,无法共享内存数据。
线程:线程也可以说是轻量级的进程,一般一个程序(或者说一个进程)包含若干个线程,线程依赖于进程,一般进程是作为分配资源的基本单位,而把线程作为独立运行和调度基本单位。同一个进程下的不同线程拥有同一个内存空间,可以共享内存数据。可以说,进程是一个容器,里面装载着线程。
协程:协程是比线程更小粒度的,也称微线程。一般进程和线程都是操作系统级别的,由操作系统去实现,而协程是编译器级别的,不被操作系统控制,完全由程序自己去实现、控制,能够避免线程切换带来的开销。协程的开销要比线程小得多。
操作系统调度CPU的最小单位是线程,多个线程交替抢占CPU的时间片。一般情况下,系统硬件的CPU是有限制的,但是线程数量远远大于CPU的数量,为使每个线程都能够抢占到CPU的时间片,系统都会在一定时间通过中断来进行CPU上执行的线程,在切换线程的时候,为了使下次调度到同一个线程执行时程序状态保持正确,在进行线程切换的时候需要保存这个线程执行的状态,也就是俗称的线程上下文。当发生线程上下文切换时,需要从操作系统的用户态切换到内核态,记录被切换的线程的上下文相关信息,同时将要执行的下一个线程的上下文信息加载到CPU寄存器中,同时从内核态切换到用户态。如果切换的线程是不同的进程,那么需要更新额外的状态信息以及内存地址空间信息,导入新的页表。
进程之间的上下文切换最大的问题在于不同进程地址空间不一样,切换地址空间导致缓存失效,所以不同进程的线程切换一般要明显慢于同一进程内的线程切换
golang中实现协程则是通过goruntine实现,具体代码级别则是通过go关键字:

func GoID() int { var buf [64]byte n := runtime.Stack(buf[:], false) idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] id, err := strconv.Atoi(idField) if err != nil { panic(fmt.Sprintf("cannot get goroutine id: %v", err)) } return id } func GoroutineTest() { fmt.Println("this is a goroutine ",GoID()) }func main() { go GoroutineTest() fmt.Println("this is main ",GoID()) time.Sleep(time.Duration(1000)) // 打印 // this is main1 // this is a goroutine6}

【golang|golang中并发、gorutine】在这里通过go关键字,采用另外一个协程去调度GoroutineTest方法。
需要注意的是,使用go关键字创建Goroutine的时候,被调用的函数即使有返回值也会被忽略,如果需要返回数据,或者和其他Goroutine共享数据,则必须通过通道channel
Goroutine的调度执行是由golang自己实现的,基于G-P-M模型,其中G代表Goroutine也就是golang的协程;P代表Processor,golang的逻辑处理器;M代表Machine,代表的则是实际的线程。
在任一时刻,一个P可能再起本地包含多个G,但是一个P在任一时刻只能绑定一个M。需要注意的是,一个G并不是定绑定在一个P上的,有多重情况会导致一个P中的G转移到其他的P中。同样,一个P只能对应一个M,但是具体对应哪一个M也不是固定的。
其实,这就有点类似于P相当于是一个队列的概念,将G放入到P中,然后给M调度执行。
golang中创建Goroutine的代价很小,每个Goroutine的堆栈只有几kb大小,能够根据程序的需要进行增长和收缩。
Goroutine是通过抢占式任务处理
下面说几个系统级相关的方法:
// 设置程序可用的最大CPU数量 runtime.GOMAXPROCS(8)// 出让当前Goroutine的时间片,类似于java中的Thread.yield runtime.Gosched()

    推荐阅读