零基础学习Go的Day10| 青训营笔记

82 阅读2分钟

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

1.复习已学知识

  • 复习并发编程的基本知识
  • 复习Goroutine
  • 复习WaitGroup

2.观看Go语言进阶与依赖管理

  • 并发安全 Lock
    昨天学习了 WaitGroup,让我们将一个经典的并发安全问题,运行如下代码:
var x int64

func addWithoutLock() {
    for i := 0; i < 2000 ; i++ {
        x += 1
    }
}

func main(){
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock:",x)
}

这是一个很简单的程序,由 5 个协程各自将 x 值 +2000。按理来说,期望的输出结果应该是 10000。 但是运行后,得到结果(并不是每次都会得到这样的结果,如果没能复现建议多试几次):

WithoutLock: 9446

这很奇怪,本应输出 10000 的代码,为什么输出结果始终不足 10000 呢。 这就要扯到并发安全这件事情了。事实上对一个变量进行修改并不是一个 原子操作。原子操作意味着,该操作是一步到位的,但是修改一个变量需要经过取出变量值,修改取出的值,将值存回变量三个步骤,考虑这么一个情况:i 值当前是 1000,当协程 1 将这个值取出时,协程 2 和协程 3 已经将该值取出,修改为 10011002 并存回 i,这时当协程 1 将他拿到的那个 1000 +1 并存回变量 i 时,变量又变回了 1001,于是就产生了错误。

为了解决该问题,我们引入并发锁 sync.Mutex,修改代码:

var (
    x    int64
    lock sync.Mutex
)

func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}

func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)
}

并发锁的原理是,当第一次调用 Lock 方法时,什么都不会发生,但当第二次调用 Lock 方法时,该调用便会立刻阻塞协程,直到有程序调用 Unlock 方法解锁。

并发锁的使用要十分谨慎,请尽在有并发安全的代码中使用并发锁,因为并发锁的使用实际上将并行的程序串行化,会导致显著降低性能;同时,不当的锁使用也可能导致死锁(DeadLock)等问题发生。

除了采用锁的方式保证并发安全,对于单个值的修改而不是一段代码的执行,更推荐引入标准库中的 sync/atomic 包来实现。其他语言也有类似的机制来确保并发安全,例如 Java 的 AtomicInteger 等类。

  • Channel
    你是否有想过如何在协程与协程之间进行数据传输和通信?你可能会说,这不是很容易的事吗,直接维护一个数组或者结构体实例之类的,然后不同协程按需取用内部的数据就行啦。但是事实上,这种通过共享内存实现的数据通信是十分危险的,可能会遇到位置的问题。Go 语言建议我们通过其内置的 通道(Channel) 功能来进行数据通信,从而共享内存。

可以通过如下方式声明一个 Channel:

ch := make(chan int)

声明了一个名为 ch无缓冲 Channel,并指定 Channel 的传输数据类型为 int

无缓存 Channel 意味着,一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。

可以通过如下方式声明一个带缓冲区的 Channel:

ch := make(chan int, 3)

声明一个名为 ch 的有三个缓冲区的 Channel,并指定 Channel 的传输数据类型为 int

这意味着,该 Channel 内可以有最多 3 个数据未被接收方接收,此时,发送方可以直接发送数据而不必收到阻塞,如果超出缓冲区(在本例中为发送第四个数据,且缓冲区被前三个数据占满),则依旧会被阻塞。

可以通过如下方式向 Channel 发送数据:

ch <- v // 假设 v 是一个 int 变量

然后,通过如下方式从 Channel 中接受数据:

v := <-ch // 赋值给 v 变量

可以通过使用 for range 的方式来从一个 Channel 中取出所有数据:

for i := range ch {
    fmt.Println(i)
}

for range 将会始终读取一个 Channel 中发送的数据,直到该 Channel 被关闭。

当一个 Channel 被使用完毕,应当调用 close 函数关闭 Channel:

close(ch)

以下代码是一个由三个协程组成的程序:第一个协程负责生成 0-9 的数字并通过 Channel 发送给第二个协程;第二个协程接收收到的数字,并将数字进行平方计算,然后将结果发送给主协程;主协程遍历接收到的结果并输出:

func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3)
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest {
        println(i)
    }
}