• golang-goroutine和channel


    goroutine

    在go语言中,每一个并发的执行单元叫做一个goroutine

    这里说到并发,所以先解释一下并发和并行的概念:

    并发:逻辑上具备同时处理多个任务的能力

    并行:物理上在同一时刻执行多个并发任务

    当一个程序启动时,其主函数即在一个单独的goroutine中运行,一般这个goroutine是主goroutine

    如果想要创建新的goroutine,只需要再执行普通函数或者方法的的前面加上关键字go

    通过下面一个例子演示并发的效果,主goroutine会计算第45个斐波那契函数,在计算的同时会循环打印:-|/  

    这里需要知道:当主goroutine结束之后,所有的goroutine都会被打断,程序就会退出

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func spinner(delay time.Duration) {
        for {
            for _, r := range `-|/` {
                fmt.Printf("
    %c", r)
                time.Sleep(delay)
            }
        }
    }
    func fib(n int) int {
        //斐波那契数列
        if n < 2 {
            return n
        }
        return fib(n-1) + fib(n-2)
    }
    
    func main() {
        go spinner(100 * time.Millisecond)
        const n = 45
        fibN := fib(n)
        fmt.Printf("
    Fib(%d)=%d
    ", n, fibN)
    }

    当第一次看到go的并发,感觉真是太好用了!!!!

    所以在网络编程里,服务端都是需要同时可以处理很多个连接,我们看一下下面的服务端和客户端例子

    服务端:

    package main
    
    import (
        "io"
        "log"
        "net"
        "time"
    )
    
    func handleConn(c net.Conn) {
        defer c.Close()
    
        _, err := io.WriteString(c, time.Now().Format("15:04:05
    "))
        if err != nil {
            return
        }
        time.Sleep(1 * time.Second)
    }
    
    func main() {
        listener, err := net.Listen("tcp", "localhost:8000")
        if err != nil {
            log.Fatal(err)
            return
        }
        for {
            conn, err := listener.Accept()
            if err != nil {
                log.Print(err)
                continue
            }
            go handleConn(conn)
        }
    }

    客户端

    package main
    
    import (
        "io"
        "log"
        "net"
        "os"
    )
    
    func mustCopy(dst io.Writer, src io.Reader) {
        // 从连接中读取内容,并写到标准输出
        if _, err := io.Copy(dst, src); err != nil {
            log.Fatal(err)
        }
    }
    func main() {
        conn, err := net.Dial("tcp", "localhost:8000")
        if err != nil {
            log.Fatal(err)
        }
        mustCopy(os.Stdout, conn)
    }

    Channel

    channel是不同的goroutine之间的通信机制。

    一个goroutine通过channel给另外一个goroutine发送信息。

    每个channel 都有一个特殊的类型,也就是channel可以发送的数据的类型

    我们可以通过make创建一个channel如:

    ch := make(chan int)  这就是创建了一个类型为int的channel

    默认我们这样创建的是无缓存的channel,当然我们可以通过第二个参数来设置容量

    ch := make(chan int,10)

    注意:channel是引用类型,channel的零值也是nil

    两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结
    果为真。一个channel也可以和nil进行比较。 

    因为channel是在不同的goroutine之间进行通信的,所以channel这里有两种操作:存数据和取数据,而这里两种操作的

    方法都是通过运算符:<-

    ch <- x  这里是发送一个值x到channel中

    x = <- ch 这里是从channel获取一个值存到变量x

    <-ch 这里是从channel中取出数据,但是不使用结果

    close(ch) 用于关闭channel

    当我们关闭channel后,再次发送就会导致panic异常,但是如果之前发送过数据,我们在关闭channel之后依然可以执行接收操作

    如果没有数据的话,会产生一个零值

    基于channel发送消息有两个重要方面,首先每个消息都有一个值,但是有时候通讯的事件和发送的时刻也同样重要。

    我们更希望强调通讯发送的时刻时,我们将它称为消息事件。有些消息并不携带额外的信息,它仅仅是用做两个goroutine之间的同步,这个时候我们可以用struct{}空结构体作为channel元素的类型

     无缓存的channel

    基于无缓存的channel的发送和接受操作将导致两个goroutine做一次同步操作,所以无缓存channel有时候也被称为同步channel

    串联的channel (Pipeline)

    channel也可以用于多个goroutine连接在一起,一个channel的输出作为下一个channel的输入,这种串联的channel就是所谓的pipeline

    通过下面例子理解,第一个goroutine是一个计算器,用于生成0,1,2...形式的整数序列,然后通过channel将该整数序列

    发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine

    第三个goroutine是一个打印程序,打印收到的每个整数

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        naturals := make(chan int)
        squares := make(chan int)
    
        go func() {
            for x := 0;; x++ {
                naturals <- x
            }
        }()
    
        go func() {
            for {
                x := <-naturals
                squares <- x * x
            }
        }()
    
        for {
            fmt.Println(<-squares)
            time.Sleep(100 * time.Millisecond)
        }
    }

    但是如果我把第一个生成数的写成一个有范围的循环,这个时候程序其实会报错的。

    把for x := 0;; x++改成for x := 0; x < 100; x++,报错如下:fatal error: all goroutines are asleep - deadlock!

    所以就需要想办法让发送知道没有可以发给channel的数据了,也让接受者知道没有可以接受的数据了

    这个时候就需要用到close(chan)

    当一个channel被关闭后,再向该channel发送数据就会导致panic异常

    当从一个已经关闭的channel中接收数据,在接收完之前发送的数据后,并不会阻塞,而会立刻返回零值,所以在从channel里接受数据的时候可以多获取一个值如:

    go func(){
       for {
          x ,ok := <-naturals
          if !ok{
             break
          }
          squares <- x*x
       }
       close(squares)
    }()
    第二位ok是一个布尔值,true表示成功从channel接受到值,false表示channel已经被关闭并且里面没有值可以接收

    单方向的channel

    当一个channel作为一个函数的参数时,它一般总是被专门用于只发送或者只接收

    chan <- int :表示一个只发送int的channel,只能发送不能接收

    < chan int : 表示一个只接受int的channel,只能接收不能发送

    当然在有时候我们需要获取channel内部缓存的容量,可以通过内置的cap函数获取

    而len函数则返回的是channel内实际有效的元素个数

    基于select的多路复用

     这里先说一个拥有的知识点:time.Tick函数

    这个函数返回一个channel,通过下面代码进行理解:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        tick := time.Tick(1 * time.Second)
        for countDown := 10; countDown > 0; countDown-- {
            j := <-tick
            fmt.Println(j)
        }
    }

    程序会循环打印一个时间戳

    select 语句:

    select {
     case <-ch1:
         // ...
     case x := <-ch2:
         // ...use x...
     case ch3 <- y:
         // ...
     default:
        // ... 
    }

    select语句的形式其实和switch语句有点类似,这里每个case代表一个通信操作

    在某个channel上发送或者接收,并且会包含一些语句组成的一个语句块 。

    select中的default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑

    channel 的零值是nil,  并且对nil的channel 发送或者接收操作都会永远阻塞,在select语句中操作nil的channel永远都不会被select到。

    这可以让我们用nil来激活或者禁用case,来达成处理其他输出或者输出时间超时和取消的逻辑

    补充:channel 概念

    类似unix中的管道pipe

    先进先出

    线程安全,多个goroutine同时访问,不需要加锁

    channel是有类型的,一个整数的channel只能存放

    定时器小例子:

    //定时器
    package main
    
    import (
        "time"
        "fmt"
    )
    
    func main() {
        t := time.NewTicker(time.Second)
        for v:= range t.C{
            fmt.Println("hello",v)
        }
    }
    // 一次性定时器
    package main
    
    import (
        "time"
        "fmt"
    )
    
    func main() {
        select{
        case <- time.After(time.Second):
            fmt.Println("after")
        }
    }
    //超时控制
    package main import (
    "time" "fmt" ) func queryDb(ch chan int){ time.Sleep(time.Second) ch <- 100 } func main() { ch := make(chan int) go queryDb(ch) t := time.NewTicker(time.Second*4) select{ case v:=<-ch: fmt.Println("result:",v) case <-t.C: fmt.Println("timeout")

    补充:不同的goroutine之间如何通信

    首先我们能够想到的有:全局变量的方式,我们先通过这种本方法来演示:

    package main
    
    import (
        "time"
        "fmt"
    )
    
    var exits [3]bool
    
    func calc(index int){
        for i:=0;i<1000;i++{
            time.Sleep(time.Millisecond)
        }
        exits[index] = true
    }
    
    func main() {
        start := time.Now().UnixNano()
        go calc(0)
        go calc(1)
        go calc(2)
    
        for{
            if exits[0] && exits[1] &&exits[2]{
                break
            }
        }
        end := time.Now().UnixNano()
        fmt.Println("finished,const:%d ms",end-start)
    }

    这种方法其实比较笨,go为我们提供了锁同步的方式 sync.WaitGroup,演示代码为:

    //等待一组goroutine执行完成
    
    package main
    
    import (
        "time"
        "fmt"
        "sync"
    )
    
    var waitGroup sync.WaitGroup
    
    func calc(index int){
        for i:=0;i<1000;i++{
            time.Sleep(time.Millisecond)
        }
        //执行完成的时候Done
        waitGroup.Done()
    }
    
    func main() {
        start := time.Now().UnixNano()
        for i:=0;i<3;i++{
            // 每次在调用之前add
            waitGroup.Add(1)
            go calc(i)
        }
        //在循环外等待wait
        waitGroup.Wait()
        end := time.Now().UnixNano()
        fmt.Println("finished,const:%d ms",end-start)
    }

    补充:关于单元测试和异常捕获

    package main
    
    import (
        "time"
        "fmt"
    )
    
    func calc(){
        // defer 定义的后面出现错误的都可以捕获到
        defer func() {
            err := recover()
            if err!=nil{
                fmt.Println(err)
            }
        }()
        var p *int
        *p = 100
    
    }
    转自https://www.cnblogs.com/zhaof/p/8393091.html
  • 相关阅读:
    CF Round #427 (Div. 2) C. Star sky [dp]
    顺时针打印矩阵
    堆 栈-相关知识【转】
    二叉树的镜像
    树的子结构
    合并两个排序的链表
    数值的整数次方
    位运算:二进制中1的个数
    斐波那契数列及其变形
    重建二叉树
  • 原文地址:https://www.cnblogs.com/justdoyou/p/10057290.html
Copyright © 2020-2023  润新知