协程(Go程) goroutine

协程:co-routine

Golang对协程的处理:

  1. 协程 => goroutine,内存几KB,可以大量使用
  2. 灵活调度,可常切换

GMP

相关资料:

Go work-stealing 调度器

goroutine 基本模型和调度器策略详解

Go 有一个可以利用多核处理器的 M:N 调度器. 任何时候, M 个 goroutine 都需要在 N 个 OS 线程上进行调度, 这些线程运行在最多 GOMAXPROCS 数量的处理器上.

G:goroutine协程

P:processor处理器(最大GOMAXPROCS)

M:thread线程

有一个 P 相关的本地和全局 goroutine 队列. 每个 M 应该被分配给一个 P. 如果被阻塞或者在系统调用中, P (们) 可能没有 M (们). 任何时候,最多只有 GOMAXPROCS 数量的 P. 任何时候, 每个 P 只能有一个 M 运行. 如果需要, 更多的 M (们) 可以由调度器创建.

调度器的设计策略

  1. 复用线程

    1. work stealing机制:未充分利用的处理器会主动去寻找其他处理器的线程并 窃取 一些
    2. hand off机制:

      如果正在运行的协程 G1 阻塞了,但是其他的调度器都在处理自己的业务,没有时间去偷 work stealing 这阻塞的 G1 上面队列中的其他协程,我们就唤醒一个线程,或者开启一个线程,将我们之前的在调度器和其他没有阻塞的协程切换过去;G 阻塞结束后,如果还要运行,就放到其他调度器 P 上面的协程队列中,如果不执行,就将 M1 线程休眠或销毁即可
  2. 利用并行:通过GOMAXPROCS限定P的个数
  3. 抢占:每个goroutine最多执行10ms(co-routine只能等待CPU主动释放)
  4. 全局G队列:当P的本地队列为空时,M先从全局队列拿G放到P的本地队列,再从其它P窃取

创建goroutine

package main
import (
    "fmt"
    "time"
)
//子go程
func newTask(){
    i := 0
    for {
        i++
        fmt.Printf("newTask Goroutine i=%d\n", i)
        time.Sleep(1 * time.Second)
    }
}
//主go程
func main() { 
    //创建子go程执行newTask()
    go newTask()
    fmt.Println("main Goroutine exit")//主go程结束后子go程也会销毁
    i := 0
    for {
        i++
        fmt.Printf("main Goroutine i=%d\n", i)
        time.Sleep(1 * time.Second)
    }
}

匿名goroutine

package main
import (
    "fmt"
    "time"
    "runtime"
)
func main() { 
    go func () {
        defer fmt.Println("A.defer")
        func () {
            defer fmt.Println("B.defer")
            runtime.Goexit()//退出当前go程
            //return //只退出当前闭包,不会退出当前go程
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
        time.Sleep(1 * time.Second)//死循环,防止主go程退出
    }
}

有参数的匿名goroutine

package main
import (
    "fmt"
    "time"
)
func main() { 
    go func (a int,b int) bool {
        fmt.Println("a=",a,",b=",b)
        return true
    }(10, 20)
    for {
        time.Sleep(1 * time.Second)//死循环,防止主go程退出
    }
}

管道 channel

定义channel
package main
import "fmt"
func main() { 
    c := make(chan int)
    go func () {
        c <- 666 //将666发送给c
    }()
    num := <- c  //从c中接收数据赋值给num
    //num ,ok := <- c //检查通道是否已关闭或为空
    fmt.Println("num=", num)//num= 666
}

注:如果num不从c中接收数据,go程会一直阻塞

缓冲
  1. 无缓冲的channel

    c := make(chan int)
  2. 有缓冲的channel:当channel已满,再向里面写数据会阻塞;当channel为空,取数据也会阻塞

    c := make(chan int, 3) //缓冲容量3
    package main
    import (
        "fmt"
        "time"
    )    
    func main() { 
        c := make(chan int, 3)
        go func () {
            defer fmt.Println("子go程结束")
            for i := 0; i < 4; i++ {
                c <- i
                fmt.Println("子go程正在运行 i=",i)
            }
        }()
        time.Sleep(1 * time.Second)
        for i := 0; i < 4; i++ {
            num := <- c
            fmt.Println("num=", num)
        }
    }
关闭channel
  1. channel不像文件一样经常去关闭,只有确实没有任何发送数据了,或想显式结束range循环,才去关闭channel;
  2. 关闭channel后无法再向channel发送数据(引发panic错误后导致接收立即返回零值)
  3. 关闭channel后,可以继续从channel中接收数据
  4. 对于nil channel,收发都会阻塞
package main
import "fmt"
func main() { 
    c := make(chan int, 3)
    go func () {
        for i := 0; i < 5; i++ {
            c <- i
        }
        close(c)//关闭channel
    }()
    for {
        //ok为true表示channel没有关闭(或有数据),false表示无数据且已关闭
        if data, ok := <- c; ok {
            fmt.Println(data)
        }else{
            break
        }
    }
    fmt.Println("main结束")
}
channel和range
range可以迭代不断操作channel (阻塞)
package main
import "fmt"
func main() { 
    c := make(chan int)
    go func () {
        for i := 0; i < 10; i++ {
            c <- i
        }
        close(c)//关闭channel
    }()
    for data := range c {
        fmt.Println(data)
    }
}
channel和select
单流程下一个go只能监控一个channel状态,select可以完成监控多个channel的状态(一般搭配for死循环)
select {
    case <- chan1:  //如果chan1成功读取到数据(可读),则执行该case处理
    case chan2 <- 1://如果chan2写入成功(可写),则执行该case处理
    default:        //如果以上都没有成功,则执行default处理
}

案例

package main
import "fmt"
func test(c, quit chan int){
   x, y := 1, 1
   for {
       select {
       case c <- x://c可以写入,则执行该case
           x = y
           y = x + y
       case <- quit://quit有值,则执行该case
           fmt.Println("quit")
           return    
       }
   }
}
func main() { 
   c := make(chan int)
   quit := make(chan int)
   //子go程
   go func () {
       for i := 0; i < 10; i++ {
           fmt.Println(<-c)//从c中读数据,否则阻塞
       }
       quit <- 0
   }()
   //主go程
   test(c, quit)
}
结果:
1
1
2
4
8
16
32
64
128
256
quit

备注:无缓冲的channel读写都会阻塞

Go Modules

GOPATH拉取依赖
go get -u xxx
GOPATH的弊端
  1. 没有版本控制概念
  2. 无法同步一致第三方版本号
  3. 无法指定当前项目引用的第三方版本号
Go Modules模式
命令作用
go mod init生成go.mod文件
go mod download下载go.mod文件中指明的所有依赖
go mod tidy整理现有依赖
go mod graph查看现有的依赖结构
go mod edit编辑go.mod文件
go mod vendor导出项目所有依赖到vendor目录
go mod verify校验一个模块是否被篡改过
go mod why查看为什么需要依赖某模块
环境变量
通过go env查看
GO111MODULE
GO Modules的开关,推荐启用

开启方式:

go env -w GO111MODULE=on
GOPROXY
设置Go模块代理

设置七牛云代理:

go env -w GOPROXY=https://goproxy.cn,direct

direct回源:如果代理找不到,则会从源拉取

GOSUMDB
保证拉取到的模块版本数据未经过篡改(建议开启)
GONOPROXY/GONOSUMDB/GOPRIVATE
配置私有库(推荐直接配置GOPRIVATE)
go env -w GOPRIVATE="*.example.com"

Go Modules初始化项目

  1. 创建go.mod

    go mod init 当前模块名称(导包时使用)
  2. 导入包(下载路径:$GOPATH/pkg/mod

    go get 包地址(例:github.com/aceld/zinx/znet)

    go.sum:罗列当前项目直接或渐渐的依赖的所有模块版本,保证今后项目依赖的版本不会被篡改

示例:

package main
import (
    "fmt"
    "github.com/aceld/zinx/ziface"
    "github.com/aceld/zinx/znet"
)
//ping test 自定义路由
type PingRouter struct {
    znet.BaseRouter
}
//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
    //先读取客户端的数据
    fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
    //再回写ping...ping...ping
    err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
    if err != nil {
        fmt.Println(err)
    }
}
func main() {
    //1 创建一个server句柄
    s := znet.NewServer()
    //2 配置路由
    s.AddRouter(0, &PingRouter{})
    //3 开启服务
    s.Serve()
}
修改包依赖关系
go mod edit -replace=xxx

IT小马
1.2k 声望166 粉丝

Php - Go - Vue - 云原生