Go进阶之并发 | 青训营笔记

59 阅读3分钟

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

前言

本文为作者参与青训营学习的课程总结,记录着我对课程内容的收获和思考,以便用于日后的复习查阅。如果所写内容出现不准确的表述或错误,欢迎各位在评论区友好指出。

1.并发和并行

并发:同一时刻,多个任务交替执行,单核CPU实现的多任务就是并发

并行:同一时刻,多个任务同时执行,多核CPU可以实现并行

image.png

2.线程与协程

线程:CPU调度的最小单位,线程是进程的一个实体。运行在内核态,一个线程可以多个协程,线程是可以抢占式的 协程:轻量级的线程,运行在用户态,非抢占式

Goroutine

在一个函数调用前使用go关键字修饰,能够为该函数调用开启一个协程

func hello(i int) {
   fmt.Println("hello goroutine:", i)
}

func main() {
   for i := 0; i < 5; i++ {
      go func(j int) {
         hello(j)
      }(i)
      time.Sleep(time.Second) //此处休眠1秒(Second为常量1000ms)
   }
}
// 结果如下
// hello goroutine: 0
// hello goroutine: 1
// hello goroutine: 2
// hello goroutine: 3
// hello goroutine: 4

上锁

但是当多个线程需要访问同一个变量时可能会出现资源抢占的情况,我们需要线程对资源进行有序的访问避免资源被随意修改,为此我们需要上锁保证并发安全。

image.png

如上图:定义了两个循环2000次的计数函数,addWithLock函数中在每次计数+1前后都会进行上锁和解锁的操作避免多个线程同时对x进行修改,最终能够得到2000*5的结果10000,而没进行锁操作的addWithoutLock函数最终得到的是一个不确定的结果(当然也有可能会得到10000,但是由于存在不稳定性,这对并发场景下的应用而言很致命的)

写到这我突然产生这样一个疑问:调用的函数里都同样是循环进行+1操作,这跟你上不上锁有关系吗?

然后我发现:我忽略了一个函数调用的时机因素。 在每次循环中如果调用函数的时间都一样,那么这时所有函数取的x都是同一个数,然后进行了x += 1,这样会导致在多次循环x + 1的结果还是一样(通俗点讲就是有些循环被吃掉了),最终出现x小于10000的计算结果。

WaitGroup

在上面的代码中我们都是手动调用time.Sleep方法进行休眠一定的时间模拟程序运行的时间花费,但在实际开发中我们并不能确切地知道程序运行所花费的时间,这就需要用到WaitGroup。

image.png image.png

我们可以使用WaitGroup设置一个计数器,在WaitGroup内部的计数器减为0前都会一直阻塞。上述代码就是在每次循环时都执行一次计数器-1,在计数器变为0之前的期间里,主协程ManyGoWait会一直阻塞,知道打印5次后才会结束。