Golang 实现并发架构

Golang 一个重要的优点就是可以容易实现并发的架构,它通过提供goroutine和channel的语言机制,很简单、方便的实现了这个优点。

goroutine 机制

goroutine简单理解就是轻量版的线程, 协程就是一个不由OS内核抢占调度,而由程序管理在用户态自管理的协作式“线程”,不用线程,就减少了OS的线程数,其优点:

  1. 省去了cpu线程切换的开销;
  2. 降低了内存消耗;
  3. 提高了cpu缓存命中率;
  4. 整体上提高了性能;
  5. 不提高硬件的前提下,提升了系统的负载能力。
Goroutine 的实现原理:

关于goroutine, 没有比较官方的文档, 我们从如下的文章翻译得到关于goroutine 调度器的说明:go-scheduler链接
Go的调度器内部有三个重要的结构:M,P,S:

  • M:代表真正的内核OS线程,和POSIX里的thread差不多,真正干活的人;
  • G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度;
  • P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。

Golang 实现并发架构_第1张图片

用户空间线程和内核空间线程之间的映射关系有:N:1,1:1和M:N

  • N:1是说,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核;
  • 1:1是说,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢;
  • M:N是说,多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。

goroutine的调度模型:
Golang 实现并发架构_第2张图片
图中看,有2个物理线程M,每一个M都拥有一个context(P),每一个也都有一个正在运行的goroutine。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

goroutine 的实现

在GO里面, 启动一个协程很简单, 只需要在函数前面加上关键字go, 通常启动一个协程, 需要配合for-loop, 保证该协程持续存在。
- 方式1:命名协程

package main

import(
    "fmt"
    "time"
)

func routeFunc() {
    for {
        fmt.Printf("run this test.")
        time.Sleep(time.Minute)
    }
}
func main() {
    go routeFunc()
    ...
}
  • 方式2: 无名协程
package main

import(
    "fmt"
    "time"
)
func main() {
    go func() {
       for {
          fmt.Printf("run this test.")
          time.Sleep(time.Minute)
       }
    }();
    ...

Channel 管道 ##

在并发体系里面,另外一个比较重要的机制就是channel,因为它可以实现goroutine之间的通信。我们知道,不同的goroutine是并行执行的,在很多场景下, 需要将执行的结构通知给main 协程,比较常见的办法就是在主协程和执行协程之间建一个channel。
channel 分为带缓存和不带缓存管道:
c1:=make(chan int) //无缓冲
c2:=make(chan int,1) //有缓冲

c1<-1
无缓冲: 不仅仅是向 c1 通道放 1,而是一直要等有别的携程 <-c1 接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞着。

有缓冲: c2<-1 则不会阻塞,因为缓冲大小是1(其实是缓冲大小为0),只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

使用channel实现多路复用

用cahnnel 配合select 实现多路复用,select的一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成的一个语句块。

package main

import (
    "fmt"
    "os"
)

func connectFunc(connChan chan int) {
     int result GetNewConnect();
     connChan <- result
}

func isAborted(abort chan int) {
    os.Stdin.Read(make([]byte, 1))
    abort <- -1
}

func main() {
    abortChan := make(chan int)
    connChan  := make(chan int)
    go isAbort(abortChan)
    go connectFunc(connChan)

    select {
    case <- connChan:
        fmt.Println("Got one new connection")
        return
    case <- abortChan:
        fmt.Println("Launch aborted!")
        return
    }
}

在这个例子里, 首先创建2个int类型的channel, connChannel, abortChan, 然后启动2个协程, 并且将结果写入相应的channel。 select是类似switch/case 的使用方式, 每当协程有结果写入abortChan或者connChan, select就能够接收到, 并且进行相应的处理。

你可能感兴趣的:(golang)