Go语言的并发机制

蹲街弑〆低调 提交于 2020-02-16 01:53:36

写在前面

并发 Concurrency

  • go的并发是通过goroutine来实现的
  • 它的使用,就是在某个操作前加上go关键字
go f(x, y, z)
  • 什么叫当前的goroutine?
    • 文字苍白,且看代码:
    package main
    import "fmt"
    
    func main() {
        fmt.println("hello,world!")
    }
    • 如果程序开始执行,那么上面的的就是当前的goroutine了
  • 什么是非当前的goroutine
    • 使用go关键词就能添加新的goroutine了
    import "fmt"
    
    func NewGoroutine() {
        fmt.Println("new goroutine!")
    }
    
    func main() {
        go NewGoroutine()       //这里就添加了新的goroutine了(运行起来才算)
        fmt.println("hello,world!")
    }
    • 但是,运行起来并没有看到打印出"new goroutine"
    • 这是因为当前的goroutine并没有义务等待这个新的goroutine执行,它自己执行完就结束了
    • 那么应该如何改变呢?Go提供了一些列管理goroutine的方法,其中最主要就是channel和Mutex了

Channel

  • Channel也是一种类型,它的定义和使用比较特别
ch := make(chan int)
ch <- v     // Send v to channel ch.
v := <-ch  // Receive from ch, and
                // assign value to v.
  • 定义一般采用make函数进行,chan关键字是固定的,后面该channel可以放置的类型,例子中的就是int
  • 它的基本使用有两种模式:
    • 其他变量往channel传送数据:ch<-v
    • 其他变量从channel接受数据:v := <-ch
  • 两种操作是互相等待的,以例子中的说明
  • 当A处的数据创送给ch时(ch <- A),它会先判断,是否其它地方需要从ch中获取数据
    • 若是:则数据从A处传到指定的地方
    • 若不是:A处的goroutine将被阻塞(其实也就是停下来),等到其它任意地方需要从ch中获取数据时,A处的goroutine才开始运行
  • 而B处需要从ch获取数据(B := <-ch),它会先判断,是否某个地方给ch传送数据
    • 若是:则数据从某个地方传送到B处
    • 若不是:B处的goroutine将被阻塞(其实也就是停下来),等到其它任意地方传送数据给ch,B处的goroutine才开始运行
  • 简单的比喻就是传送门两侧,需要等待两边的人都同意才能打开
  • 或者这样理解,如果把这个过程比喻成接力赛
    • 接力棒是数据,每个队有多个成员
    • A成员的目标就是将接力棒交给B成员,但需要奔跑,需要时间,B成员需要等待A成员(被阻塞)
  • 下面是例子代码:
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}
  • 注意到 x, y := <-c, <-c // receive from c :
    • 这段代码在当前goroutine中出现,当程序运行起来,这个位置的channel c将会等待从某处传来的数据
    • 而某处就是go关键字标记出来的两个新goroutine了:
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    • 这两处都是执行sum函数,执行的最后把结果传给channel c
    • 这就解决了当前goroutine不等待其它goroutine运行结束就自个儿运行完成的问题
  • buffered channel是允许存储多个值的channel,它的空间可以理解为一个数组
  • 相当于接力赛中,A队员要拿着多根接力棒跑,他很辛苦,不只要给B队员,还要给C队员,还要。。。。
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
  • 读者们可以试着把ch定义成普通的channel:ch := make(chan int)
  • 这样会报错,原因在于:
    • 首先这是在一个goroutine中
    • 其次,ch <-1执行时,会一直等待其它goroutine执行从ch中拿数据的操作

关键字:range,close

  • 当某个goroutine中的某个操作需要不停从channel c中获取数据时,可以用到range关键字
  • 而对于上述操作,并不需要担心当channel c不再有数据的事情,这个事情是由运行发送数据到channel c的goroutine关心的
  • 如果发送数据给channel c的goroutine不需要再发送数据了,那就需要执行close(c)的操作,否则程序会报错
package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}
  • 对于上述代码,先关注main函数
    • go fibonacci(cap(c), c)这一句新建了一个goroutine,其中cap(c)是用来计算channel c的容量
    • for i := range c这句,
      • 将不停循环判断,
      • 一有数据传送到channel c,i 就能获取该数据,并执行循环语句中的内容
      • 当channel c被关闭,循环判断将结束
  • 再看fibonacci函数
    • fibonacci数列的计算,其中for循环里的内容c <- x这句,就是向channel c传送数据,之后main中的for语句会执行相应操作
    • 最后close(c)这句,关闭channel c,之后main的for语句结束执行

关键字 select

  • select很特别,它一般会搭配for循环使用,行为上和switch语句类似
  • select语句判断是是否执行了某些操作,如果是,我就将执行XXX操作
  • switch语句判断的通常是某某值是否等于某某值这种true or false的问题
  • 代码:
package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}
  • main()中,创建了channel c和channel quit
  • 一个匿名函数作为新建goroutine的运行内容
  • 然后运行fibonacci函数(这是当前goroutine中的)
    • 在fibonacci函数中,使用for进行无限循环不停执行select语句
    • 在这里select语句会不停判断每个case语句中的操作
    • 像case c <- x,判断条件也是一种操作,所以会先把x传送给c,然后阻塞,等待其他向channel c要数据的操作
      • 这时,匿名函数(新建的goroutine)的for循环中fmt.Println(<-c)将会执行,执行完成后,又会进行等待其他操作向channel c 传递数据
      • 而在fibonacci中,case c <- x能顺利进行后,将执行x, y = y, x+y,然后进入新的循环中执行select语句
  • 过程就像上述那样,进行循环,判断,阻塞,解除阻塞。。。
  • 等到goroutine的循环结束,并执行quit <- 0后,就会执行select语句中的case <-quit的内容了,之后程序返回,当前goroutine结束
  • 当然,select也会搭配关键字default:
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}
  • 当其它case中的channel都每准备好,就会执行default中的语句

sync.Mutex

  • 解决并发冲突的办法还有比较经典的Mutex
  • Mutex可以进行加锁Lock()和解锁操作Unlock()
  • 这里不展开了,留在下一篇,也是a tour of go的最后一道习题,个人认为这个题目出得非常好,值得详细讲解
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!