Go语言并发实践

Go语言并发

  • 一、并发模型
    • CSP
    • Go语言调度模型
  • 二、goroutine
  • 三、channel
  • 四、Go并发
    • goroutine协作
      • 扇入扇出
    • 通知退出
  • 五、并发范式
    • 融合并发、缓冲、退出通知的生产者
    • 链式通道
    • 每个请求一个goroutine
    • 固定工作池
    • future模式
  • 六、Context
  • 七、参考资料

https://golang.google.cn/

一、并发模型

CSP

CSP基本思想是:
将并发系统抽象为channel和process两部分,二者相互独立,没有从属关系 ;
channel用来传递消息,消息的发送和接收有严格的时序限制;
process用于执行 。
在Go语言中,channel就是通道 process就是goroutine 。

Go语言调度模型

goroutine的调度模型抽象出来三个实体:M P G

M:machine:一个内核线程 
P:processor  Go执行一段代码的上下文环境 
G:goroutine 一个代码片段 
 
MP(内核线程+上下文环境) 关联才形成一个有效的G运行环境 
每个P包含一个可运行的G队列 runq 
 
一个Go程序最大可以使用10000个M 
runtime/debug SetMaxThreads 设置M最大值 
调用时会返回旧的M个数,且新值小于旧值会panic 
 
runtime.GOMAXPROCS 
(新值小于旧值,不更新,只返回旧值) 
改变单个Go程序间接拥有P的最大数量。 
0~256(目前Go还不能保证P在大于256情况下程序会正常保持高效) 
P的数量就是可运行G的队列数量。 
这是P的限制修改,不会影响MG 
程序运行时,默认的,P的最大值会被设置为和当前CPU总核数一致

二、goroutine

  • go的执行是非阻塞,不会等待,后面的函数返回值会被忽略
  • 调度器不能保证goroutine的执行顺序
  • 没有父子关系的概念,所有goroutine是平等的执行和调度
  • go没有暴露goroutine的id给用户,不能一个goroutine显式调用另一个。
  • Go程序函数启动顺序的示意图:

Go语言并发实践_第1张图片
Go语言程序的初始化和执行总是从main.main函数开始的。但是如果main包导入了其它的包,则会按照顺序将它们包含进main包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量,再调用包里的init函数,如果一个包有多个init函数的话,调用顺序未定义(实现可能是以文件名的顺序调用),同一个文件内的多个init则是以出现的顺序依次调用(init不是普通函数,可以定义有多个,所以也不能被其它函数调用)。最后,当main包的所有包级常量、变量被创建和初始化完成,并且init函数被执行后,才会进入main.main函数,程序开始正常执行。

  • 要注意的是,在main.main函数执行之前所有代码都运行在同一个goroutine,也就是程序的主系统线程中。因此,如果某个init函数内部用go关键字启动了新的goroutine的话,新的goroutine只有在进入main.main函数之后才可能被执行到。

三、channel

channel是goroutine之间通信和同步的重要组件。
go语言 不要通过共享内存来通信,而是通过通信来共享内存。
channel通过make创建:
创建无缓冲通道:make(chan datatype)
创建有缓冲通道:make(chan datatype,n)
go内置函数len和cap获取channel的元素数和容量。
无缓存通道的 len和cap都是0。
有缓存通道的len是没被读取的元素数,cap是整个通道的容量。
无缓冲通道可用于通信和同步,
有缓冲通道主要用于通信。
缓冲通道和消息队列类似,有削峰和增大吞吐量的功能。

<-chan T 
chan<- T 
chan T 
len元素个数 
cap容量 

接收操作符 元素/通道 <- 元素/通道
 
1.通道类型是引用类型,被初始化之前值是nil 
2.通道类型的变量是传递值的,不是保存值的,所以没有值表示法。 
它的值具有即时性,无法用字面量准确表达。 
3.发送 
发送操作会使通道复制被发送的元素 
4.取值 
<-myChan 
elem:=<-myChan 
elem,ok:=<-myChan 
 
5.close() 
重复关闭会panic 
可以读取,不可以写入 
elem,ok:=<-myChan 
当接收操作因通道关闭而结束时,ok为false,表示操作失败,否则为true 
读取未初始化的通道值,会永久阻塞 
6.range读取channel数据
for x:=range <-myChan{
      
         
         
} 
7.select 
for{
      
        select{
      
                case e1:=<-myChan: 
                  ... 
                case <-time.NewTimer(time.Second*1).C: 
                        超时操作 
                default: 
                  所有分支不满足会进入该分支 
        } 
} 
8.Ticker 定时读取
for _=range time.NewTicker(time.Second).C{
      
        select{
      
                case xxx 
        } 
}

channel引起panic

  • 向已关闭的channel写数据(尽量写入者关闭)。
  • 重复关闭channel
    channel阻塞
  • 向未初始化的channel写数据,会产生永久阻塞。
  • 向缓冲器已满的channel写数据
  • 读取没数据的channel
    读取关闭的channel
    读取关闭的channel不会阻塞或panic,可以用comma,ok语法判断缓冲器是否已关闭。

四、Go并发

goroutine协作

goroutine之间协作主要涉及 通信、同步、通知、退出四个方面
1,通信:channel。
2.同步:
1.无缓存的channel;
2.sync.WaitGroup。
3.通知:
1.增加一个channel用于异常通知数据,然后通过select收敛进行处理。
2.context标准库
4.退出:
1.借助channel和select的广播机制(close channel 实现 broadcast) 实现退出
2.context标准库

扇入扇出

编程常遇到的扇入扇出概念:
扇入:
多路通道聚合到一个通道处理,
go语言中,可以使用select聚合多条通道服务。
扇出:
将一个通道发散到多条通道处理,
go语言中,使用go关键字启动多个goroutine并发处理。

通知退出

实现原理:
增加一个channel done 用于异常通知数据,然后通过select收敛进行处理;
借助channel和select的广播机制(close channel 实现 broadcast) 实现退出。

package main

import (
    "math/rand"
    "runtime"
)

func main() {
     
    done := make(chan struct{
     })
    ch := generate(done)
    //生产者就绪,goroutine有两个:main+生产者
    println("goroutine num1:", runtime.NumGoroutine())
    //消费
    for i := 0; i < 2; i++ {
     
        println(<-ch)
    }
    close(done)//通知退出
    //已经停止生成,还可以读取通道中剩余的1个
    for res := range ch {
     
        println("res:", res)
    }
    //生成已退出,当前goroutine只剩main一个
    println("goroutine num2:", runtime.NumGoroutine())
}
//生成随机数的函数
func generate(done chan struct{
     }) chan int {
     
    ch := make(chan int)
    //生产者
    go func() {
     
    lb: // 跳出for循环
        for {
     
            select {
     
            case ch <- rand.Int()://生成随机数
                println("generate ok")
            case <-done://接到通知,退出循环
                println("done ok")
                break lb
            }
        }
        close(ch)//停止生产
    }()
    return ch
}





//结果
PS E:\mypro\company\mygrpc\gopro> go run .\main.go 
goroutine num1: 2 
5577006791947779410 
generate ok 
8674665223082153551 
generate ok 
res: 6129484611666145821 
generate ok 

五、并发范式

融合并发、缓冲、退出通知的生产者

package main

import (
    "math/rand"
    "runtime"
)

// 融合并发、缓冲、退出通知的生产者
func main() {
     
    done := make(chan struct{
     })
    ch := generateInt(done)
    for i := 0; i < 10; i++ {
     
        println(<-ch)
    }
    println("goroutine num1:", runtime.NumGoroutine())
    close(done)
    for res := range ch {
     
        println("res:", res)
    }
    println("goroutine num2:", runtime.NumGoroutine())
}

// 扇入
func generateInt(done chan struct{
     }) chan int {
     
    ch := make(chan int)
    childDone := make(chan struct{
     })
    go func() {
     
    //扇入两个生产者,可以相同或者不同
        chA := <-generate(childDone)
        chB := <-generate(childDone)
    lb:
        // 如何跳出for循环
        for {
     
            select {
     
            //随机选择生产者数据
            case ch <- chA:
                println("generate 1")
            case ch <- chB:
                println("generate 2")
            case <-done://接到退出通知,通知生产者也退出,然后退出循环
                close(childDone)//通知生产者
                break lb
            }
        }
        close(ch)
    }()
    return ch
}
func generate(done chan struct{
     }) chan int {
     
    ch := make(chan int)
    go func() {
     
        println("lb1")
    lb:
        // 如何跳出for循环
        for {
     
            select {
     
            case ch <- rand.Int():
            case <-done:
                println("okkkk")
                break lb
            }
        }
        println("return ")
        close(ch)
    }()
    return ch
}





//结果
PS E:\mypro\company\mygrpc\gopro> go run .\main.go 
lb1 
lb1 
generate 2 
6129484611666145821 
5577006791947779410 
generate 1 
generate 2 
6129484611666145821 
5577006791947779410 
generate 1 
generate 2 
6129484611666145821 
5577006791947779410 
generate 1 
generate 2 
6129484611666145821 
5577006791947779410 
generate 1 
generate 2 
6129484611666145821 
6129484611666145821 
goroutine num1: 4 
generate 2 
generate 1 
okkkk 
return 
okkkk 
return 
res: 5577006791947779410 
goroutine num2: 1

链式通道

package main

// 管道
/*
通道分为两个方向:读、写
假如函数的输入输出是相同的chan类型,则函数可以调用相同类型的函数生成调用链
*/
func main() {
     
    in := generate()        //0~9
    out := chain(chain(in)) //2~11
    for res := range out {
     
        println("res:", res)
    }
}

// 生产者
func generate() chan int {
     
    ch := make(chan int)
    go func() {
     
        for i := 0; i < 10; i++ {
     
            ch <- i
        }
        close(ch)
    }()
    return ch
}

// 函数将channel内的数据统一做处理
func chain(in chan int) chan int {
     
    out := make(chan int)
    go func() {
     
        for res := range in {
     
            out <- res + 1
        }
        close(out)
    }()
    return out
}



//结果
PS E:\mypro\company\mygrpc\gopro> go run .\main.go 
res: 2 
res: 3 
res: 4 
res: 5 
res: 6 
res: 7 
res: 8 
res: 9 
res: 10 
res: 11

每个请求一个goroutine

即,来一个任务起一个goroutine去处理

net/http包 server.go
func Serve(l net.Listener, handler Handler) error {
     
    srv := &Server{
     Handler: handler}
    return srv.Serve(l)
}
func (srv *Server) Serve(l net.Listener) error {
     
    ...
    for {
     
        rw, e := l.Accept()

        if cc := srv.ConnContext; cc != nil {
     
            ctx = cc(ctx, rw)
            if ctx == nil {
     
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        //启动一个独立的goroutine处理web请求
        go c.serve(ctx)
    }
}

固定工作池

服务器编程使用最多的,
就是通过线程池来提高服务的并发处理能力,
go语言中,
可以使用固定数量的goroutine作为工作池。

future模式

package main

import "time"

/*
一个流程需要调用多个子调用,这些子调用之间没有依赖,
此时使用future模式可以减少串行耗费的时间:
1.使用channel作为函数参数
2.启动goroutine调用函数
3.通过channel传入参数
4.做其他可以并行的操作
5.channel异步获取结果

future最大的好处是将函数的通过调用转换成异步调用,
适用于任务需要多个子调用且子调用之间相互独立的场景。


一个例子,体验下思想
*/
type query struct {
     
    sql    chan string
    result chan string
}

func execQuery(q query) {
     
    go func() {
     
        sql := <-q.sql
        // ...
        q.result <- "result from " + sql
    }()
}

func main() {
     
    q := query{
     
        sql:    make(chan string, 1),
        result: make(chan string, 1),
    }
    go execQuery(q)
    q.sql <- "select * from table"
    // 做其他事情
    time.Sleep(time.Second * 1)
    // 获取结果
    println("result:", <-q.result)
}
//结果
PS E:\mypro\company\mygrpc\gopro> go run .\main.go 
result: result from select * from table

六、Context

context包提供的功能:
1.退出通知机制
2.元数据传递
可以传递一些日志信息、调试信息或不影响主逻辑的可选数据。
context在grpc中的使用
Go语言并发实践_第2张图片

七、参考资料

Go语言核心编程

你可能感兴趣的:(go,go语言,golang,并发编程)