Go语言进阶 | 青训营笔记

91 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

Go 语言进阶与依赖管理

3 并发编程

并发:多线程程序在一个核的cpu上运行

并行:多线程程序在多个核的cpu上运行

Go 可以充分发挥多核优势,高效运行

3.1 goroutine

goroutine 是通过 Go 的 runtime 管理的一个线程管理器。

goroutine 通过 go 关键字实现了,其实就是一个普通的函数。

携程:用户态,轻量级线程,栈MB级别

线程:内核态,线程跑多个携程,KB级别

package main
​
import (
    "fmt"
    "runtime"
)
​
func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched() //runtime.Gosched()表示让 CPU 把时间片让给别人,下次某个时候继续恢复执行该goroutine。
        fmt.Println(s)
    }
}
func main() {
    go say("world") //开一个新的 Goroutines 执行
    say("hello")    //当前 Goroutines 执行
}

3.2 CSP(Communicating Sequential Processes)

设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。

默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,

需要在我们的程序中显示的调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线

程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。

如果 n < 1,不会改变当前设置。以后 Go 的新版本中调度得到改进后,这将被移除。

3.2.1 channels

goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。那么 goroutine 之间如

何进行数据的通信呢,Go 提供了一个很好的通信机制 channel。channel 可以与 Unix shell

中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel 类

型。定义一个 channel 时,也需要定义发送到 channel 的值的类型。注意,必须使用 make

创建 channel:

ci := make(chan int)
​
cs := make(chan string)
​
cf := make(chan interface{})

channel 通过操作符<-来接收和发送数据

ch <- v // 发送 v 到 channel ch.

ch <- v // 发送 v 到 channel ch.
​
v := <-ch // 从 ch 中接收数据,并赋值给 v

我们把这些应用到我们的例子中来:

package main
​
import "fmt"func sum(a []int, c chan int) {
    sum := 0
    for _, v := range a {
        sum += v
    }
    c <- sum // send sum to c
}
​
func main() {
    a := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c // receive from c
    fmt.Println(x, y, x+y)
}

默认情况下,channel 接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得

Goroutines 同步变的更加的简单,而不需要显式的 lock。所谓阻塞,也就是如果读取

(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,

直到数据被读出。无缓冲 channel 是在多个 goroutine 之间同步很棒的工具。

3.2.2 Buffered Channels(缓冲通道)

上面我们介绍了默认的非缓存类型的 channel,不过 Go 也允许指定 channel 的缓冲大小,

很简单,就是 channel 可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储 4 个

元素的 bool 型 channel。在这个 channel 中,前 4 个元素可以无阻塞的写入。当写入第 5 个

元素时,代码将会阻塞,直到其他 goroutine 从 channel 中读取一些元素,腾出空间。

ch := make(chan type, value)
value == 0 ! 无缓冲(阻塞)
value > 0 ! 缓冲(非阻塞,直到 value 个元素)
3.2.3 并发安全LOCK

保证并发的安全,临界区问题

var (
    x    int64
    lock sync.Mutex
)
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
3.2.4 Range 和 Close

可以通过 range,像操作 slice 或者 map 一样操作缓存类型的 channel

package main
​
import (
    "fmt"
)
​
func fibonacci(n int, c chan int) {
    x, y := 1, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)//生产者通过关键字 close 函数关闭 channel,关闭channel 之后就无法再发送任何数据了
}
func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

在消费方可以通过语法 v, ok := <-ch 测试 channel是否被关闭。如果 ok 返回 false,那么说明 channel 已经没有任何数据并且已经被关闭。

记住应该在生产者的地方关闭 channel,而不是消费的地方去关闭它,这样容易引起 panic另外记住一点的就是 channel 不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束 range 循环之类的。

3.2.5 Select

我们上面介绍的都是只有一个 channel 的情况,那么如果存在多个 channel 的时候,我们

该如何操作呢,Go 里面提供了一个关键字 select,通过 select 可以监听 channel 上的数据

流动。

select 默认是阻塞的,只有当监听的 channel 中有发送或接收可以进行时才会运行,当多

个 channel 都准备好的时候,select 是随机的选择一个执行的。

package main
​
import "fmt"func fibonacci(c, quit chan int) {
    x, y := 1, 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)
}

在 select 里面还有 default 语法,select 其实就是类似 switch 的功能,default 就是当监听的

channel 都没有准备好的时候,默认执行的(select 不再阻塞等待 channel)

select {
case i := <-c:
 // use i
default:
 // 当 c 阻塞的时候执行这里
}
3.2.6 超时
package main
​
import "time"func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
            case v := <-c:
                println(v)
            case <-time.After(5 * time.Second):
                println("timeout")
                o <- true
                break
            }
        }
    }()
    <-o
}
​
3.2.7 runtime goroutine

runtime 包中有几个处理 goroutine 的函数:

• Goexit

退出当前执行的 goroutine,但是 defer 函数还会继续调用

• Gosched

让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从

该位置恢复执行。

• NumCPU

返回 CPU 核数量

• NumGoroutine

返回正在执⾏行和排队的任务总数

• GOMAXPROCS用来设置可以运行的 CPU 核数

未完~