• go 学习 (五):goroutine 协程


    一、goroutine 基础

    定义

    • 使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作,此机制在Go中称作 goroutine
    • goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
    • Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

    语法 

    // 普通函数创建 goroutine 语法
    go  函数名称(参数列表)    
    
    
    // 匿名函数创建 goroutine 语法
    go func(形参列表) {函数体...}(实参列表)
    
    
    // 使用 go 创建 goroutine 后,函数的返回值会被忽略

    普通函数创建 goroutine

    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    
    func listElem(n int) {
        for i:=0; i<n; i++ {
            fmt.Println(i)
        }
    }
    
    func main() {
        go listElem(10)        // 若 main 函数中仅有这一句代码,控制台将没有任何输出,因为 main 创建协程后立即推出了,还未来得及执行协程
    
        //添加以下代码
        var input string
        fmt.Scanln(&input)        // 需要用户输入任意字符 main 才会结束
    }

    匿名函数创建 goroutine 

    func main() {
        go func() {
            var times int
            for {
                times++
                fmt.Println("tick", times)
                time.Sleep(time.Second)
            }
        } () 
    }

    二、channel

    channel 基础

    • 一个 channel 是一个通信机制,可让一个 goroutine 通过 channel 给另一个 goroutine 发送消息。
    • 每个 channel 都有一个特殊的类型,也就是 goroutine 之间进行通信时能够传递的数据类型;eg:若要通过 channel 发送 int 类型的数据,那么  chan int  即可。
    • 通道之间能够共享的数据类型有:内置类型、命名类型、结构类型、引用类型的值、指针。
    • 通道遵循 “先进先出” 原则,保证收发数据的顺序,并且在同一时间只能有一个 goroutine 访问通道进行发送消息或者读取消息。

    语法

    // 声明之后必须配合 make 才能使用
    var  通道变量  chan  通道类型            // 声明通道;通道类型,通道内的数据类型
    
    通道实例  =  make(chan  数据类型)        // 创建通道;数据类型,通道内传输的元素类型
    
    
    // eg:
    var  ch1  chan  string
    ch1   =  make(chan  string)
        
    ch2 := make(chan int)                        // 创建整型类型的通道
    
    ch3 := make(chan interface{})            // 创建空接口类型的通道,可存放任意格式            
    
    ch4 := make(chan *Equip)                  // 创建 Equip 指针类型的通道,可存放 *Equip
    
    
    // 关闭通道
    close(ch1)

    使用通道发送数据

    // 通道发送数据的语法:  "<-"  操作符
    通道变量  <-  值                // 值可以是:变量、常量、表达式、函数返回值、但值的类型必须与通道声明的元素类型一致
    
    
    // eg
    ch := make(chan interface{})            // 创建空接口通道
    ch <- 0                                            // 向通道内放入 0
    ch <- "Hello World"                          // 向通道内放入 字符串
        
    
    // 附:向通道内发送数据时,若没有对应的接收方读取数据,那么发送数据的操作将会一直堵塞。

    使用通道接收数据

    • 通道数据的收发操作在两个不同的 goroutine 中进行
    • 接收方会一直阻塞到发送方发送数据为止,同样的,发送方也会一直阻塞到接收方读取数据为止。
    • 通道每次只能接收一个数据元素
    // 接收数据同样使用  "<-" 操作符
    
    data := <-ch        // 阻塞接收数据
    
    data, ok := <-ch        // 非阻塞接收数据,方未接收到数据时,data为通道类型的零值,ok为false;此方式会造成CPU高占用,实现接收超时检测会配合 select 和 计时器channel进行,一般较少使用
    
    <-ch                        // 接收任意数据,并忽略接收到的数据
    
    
    // 循环接收
    for data := range ch {
        // do something
    }

    单向通道

    var 通道变量 chan<- 通道类型        // 只写 channel
    var 通道变量 <-chan 通道类型        // 只读 channel
    var 通道变量 chan 通道类型                // 可读可写 channel
    
    
    // eg
    var wc chan<- string        
    var rc <-chan string        
    var ch chan string    
    
    
    // 关闭 channel
    close(ch)    

      

    无缓冲通道

    在接收数据前,没有能力,保存任何值,的通道。它要求 发送通道和接收通道 必须同时准备好,才能完成发送和接收操作;如果有一方通道a没有准备好,那么另一方通道b会阻塞等待着通道a。

    package main
    
    import (
        "fmt"
        "math/rand"
        "sync"
        "time"
    )
    
    
    var wg sync.WaitGroup
    
    func init() {
        rand.Seed(time.Now().UnixNano())
    }
    
    func player(name string, court chan int) {
        /**
        击球比赛 大致流程:
            创建一个数据类型是 int的channel
              创建两个 goroutine(选手),互相进行击球,由于 player 中第一行代码是在阻塞等待接球(从channel中获取数据),所以在main当中需要发出第一个球
              player:
                  启动两个 goroutine 后,总有一方会抢先从 channel 中取出数据,那么另一方处于阻塞等待的状态。
                  然后判断 通道channel 是否关闭(若已关闭,ok=false )
                  若已经关闭,代表是对方关闭的通道对方输球,则当前选手获胜,退出游戏。
                  若上一个判断不成立,则接下来选择一个随机数 并 判断是否可以被 22 整除,以此来决定是否丢球(向通道写入数据,让对方获取【接球】)
                      若无法被整除,当前选手输球,直接关闭通道并退出,此时执行权在另一方选手,从通道取出数据时,发现通道已关闭,那么打印胜利信息。
                      若可以整除,显现当前的击球数,并将其写入到通道,让对方接球。
    
              以下代表表示主 goroutine 需要等待另两个 goroutine 结束之后才能结束 main
                  wg.Add(2)
                  defer wg.Done()
                  wg.Wait()
        **/
    
        defer wg.Done() // 函数退出时调用 Done 函数,以此通知 main 函数,当前函数的工作已完成
    
        for {
            ball, ok := <-court // 等待接球
            if !ok {            // 若通道关闭,则胜利
                fmt.Printf("Player %s Won
    ", name)
                return
            }
    
            // 选择随机数,使用此数判断是否丢球
            n := rand.Intn(100)
            if n % 22 == 0 {
                fmt.Printf("Player %s Missed
    ", name)
    
                // 关闭通道,表输球
                close(court)
                return
            }
    
            // 显示击球数,且击球数 +1
            fmt.Printf("Player %s Hit %d
    ", name, ball)
            ball++
    
            // 击球给对方
            court <- ball
        }
    
    }
    
    
    func main() {
    
        // 击球比赛
        court := make(chan int)
        wg.Add(2)            // 表:需等待两个 goroutine
        go player("Juan", court)    // 启动两个选手
        go player("Xiao", court)
        court <- 1        // 发球
        wg.Wait()        // 等待结束
    }
    击球比赛 
    跑步接力赛

     带缓冲通道  &  关闭通道

     在数据被接受前,可以存储一个或多个值的通道,此类型通道不强制要求 两方 goroutine 必须同时完成发送和接收 。阻塞发生的情况:当通道中没有需要接收的值时接受动作会被阻塞;当通道中没有可用的缓冲区来容纳要发送的值时发送动作会被阻塞。

    func main() {
        // 带缓冲通道:  创建语法,  通道实例 := make(chan 通道类型,缓存大小)
        bufferCh := make(chan int, 3)
        fmt.Printf("put data before, channel length: %d
    ", len(bufferCh))            // 查看放入数据前通道大小
        bufferCh <- 1
        bufferCh <- 2
        bufferCh <- 3
        close(bufferCh)        // 通道关闭仍然可以从通道中读取数据,但不可发送数据,会引发panic错误,当已关闭的通道中 也没有数据后,不会阻塞获取数据操作,将会返回零值
        fmt.Printf("put data after, channel length: %d
    ", len(bufferCh))            // 查看放入数据后通道大小
    
        data1 := <-bufferCh
        fmt.Printf("get first data: %d
    ", data1)
        data2 := <-bufferCh
        fmt.Printf("get secondly data: %d
    ", data2)
        data3 := <-bufferCh
        fmt.Printf("get thirth data: %d
    ", data3)
        data4 := <-bufferCh
        fmt.Printf("get next data, bur no data anymore, it is %d
    ", data4)
    }    

    区别

    • 无缓冲通道保证双方的 goroutine 会在同一时间进行数据的交换(数据交换时,会把两个 goroutine 一起锁住,只有一个goroutine将数据交棒给另一个goroutine之后才会解除堵塞);
    • 带缓冲通道是在无缓冲通道的基础上,为通道增加了一个有限大小的存储空间,带缓冲通道在发送时 不需要等待接收方接收完 才完成发送,也不会发生堵塞,直到存储空间满了才会发生堵塞;同样的,如果通道中已有数据,接收时并不会发生堵塞,直到通道中没有数据了才会发生堵塞。
    • 举个栗子:上门取件服务(无缓冲通道)   和   快递柜取件(带缓冲通道)
      • 上门取件:发送方(我)  &  接收方(快递员),当我把快递准备好之后,打电话给快递员跟他说明地址(让快递员准备好)之后,才能把快递寄出去;如果快递员还没有来到我的地址,我就得阻塞等待;同样,如果我还没准备好快递,快递员就等待我把快递准备好之后,他才能把快递带走。
      • 快递柜:发送方(快递员)  & 接收方(我),快递到了之后快递员会把快递塞进快递柜里,然后再发一条短信给我让我取快递,但是我不需要立马去取快递,快递员也不需要等我取完快递之后他才能送下一个快递;只有当快递柜满了之后,快递员就不能塞快递了,就会阻塞等待有空位之后才能继续放入快递;而我也只有当快递柜里啥都没有的时候才会等着快递来。

    channel 超时机制

    // 首先,我们实现并执行一个匿名的超时等待函数
    timeout := make(chan bool, 1)
    go func() {
        time.Sleep(1e9) // 等待1秒钟
        timeout <- true
    }()
    
    // 然后我们把timeout这个channel利用起来
    select {
        case <-ch:
        // 从ch中读取到数据
        case <-timeout:
        // 一直没有从ch中读取到数据,但从timeout中读取到了数据
    }

     channel 多路复用

    // 一次性接收多个通道的数据,当操作发生时,会执行对应case语句中的响应
    select{
        case 操作1:
            响应操作1
        case 操作2:
            响应操作2
        …
        default:
            没有操作情况
    }
    
    
    select 多路复用中可以接收的样式
    操   作语句示例
    接收任意数据 case <- ch;
    接收变量 case d :=  <- ch;
    发送数据 case ch <- 100;
  • 相关阅读:
    B. Connecting Universities DFS,无向树
    HDU 5808 Price List Strike Back bitset优化的背包。。水过去了
    uva 6910
    HDU 5778 abs 数学
    Invitation Cards POJ 1511 SPFA || dij + heap
    HDU 2243 考研路茫茫——单词情结 求长度小于等于L的通路总数的方法
    cmd链接mysql
    如何设置远程访问mysql
    ERROR 1045 (28000): Access denied for user'root'@'localhost'(using password:YES)51Testing软件测试网-H*?U)}$h }P6H4H
    String ,StringBuffer,StringBuilder的区别(转)
  • 原文地址:https://www.cnblogs.com/hsmwlyl/p/11800410.html
Copyright © 2020-2023  润新知