零基础之go语言5 | 青训营笔记

34 阅读4分钟

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

前面1-4的笔记仅仅是go语言语法基础。从本次笔记开始,我将记录更加进阶的语法、包等

本次笔记将记录并发协程的相关概念操作。

Goroutine

在介绍Goroutine之前,需要引出以下几个概念

1.进程
进程是应用程序的启动实例,是系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间,不同进程通过进程间的通信方式来通信。
    
2.线程
线程从属于进程,是进程中的一个实体,线程是CPU调度的基本单位,一个线程由线程ID、当前指令指针、寄存器集合和堆栈组成。
线程不拥有自己的系统资源,它与同属于同一进程的其他线程共享进程所拥有的全部资源,多个线程之间通过共享内存等线程间的通信方式来通信,线程拥有自己独立的栈和共享的堆。

3.协程
协程可以理解为轻量级线程,一个线程可以拥有多个协程。
与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程,这也就是GO在语言层面直接支持协程的含义。

所以,标题的Goroutine正是指上面三个之中的协程用户可以使用go关键字来创建协程

协程的优点:
1、使用协程与不使用协程对比:,协程是一种并发机制,是一串运行在进程中的任务代码,只是这些任务代码可以交叉运行。
2、协程与进程线程对比:协程运行在用户态,能减少上下文切换带来的开销,并且协程调度器把可运行的协程逐个调度到线程中执行,同时及时把阻塞的协程调度出线程,从而有效的避免了线程的频繁切换,达到了使用少量的线程实现高并发的效果,但是对于一个线程来说每一时刻只能运行一个协程。

下面将通过代码直观的了解到协程

func hello(i int) {
   println("hello Parallel : " + fmt.Sprint(i))
}

func Hello() {
   for i := 0; i < 5; i++ {
      go func(j int) {
         hello(j)
      }(i)
   }
   time.Sleep(time.Second)
}
代码目的:通过协程输出hello0-hello4
//go关键字后面需要跟随一个函数,可以是已定义的函数,也可以是如本例一样新构造一个一次性函数

输出结果如图:

image.png 由此可见,协程的运行是不分先后,可以说是随机的,同时也证明了协程的并发机制。这样做就会比单独for循环一个hello0-hello4更快,开销更低。

Channel

channel是指golang中的通道,它是用于携程中的通信,遵循先进先出,go不提倡通过共享内存来通信,而是提倡通过通信来实现内存共享,同时,channel是线程安全的。

以下为青训课的概念图image.png

channel通过make关键字创建:intChan = make(chan int)intChan = make(chan int, 3),而这两个区别在于创建的channel是否有3个int类型的缓冲区。

image.png

向创建的channel写入数据:intChan<- 10
向创建的channel取出数据:num2 = <-intChan(先进先出)
//以下为示例
func CalSquare() {
   println("this is channel")
   src := make(chan int)
   dest := make(chan int, 3)
   go func() {
      defer close(src)
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()
   //在此协程中得到10个数,并向channel src写入

   go func() {
      defer close(dest)
      for i := range src {
         dest <- int(math.Pow(float64(i), 2))
      }
   }()
   //将写入src的数据取出,经过第二个协程做平方处理最后存入channel dest

   for i := range dest {
      println(i)
   }
   //遍历dest并打印dest中的数据

结果如图 image.png

Lock

众所周知,锁有排它锁(X)、享锁(S)以及意向锁(I)

本部分介绍一下go中如何使用锁,以及是否加锁对协程的区别

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

   }
}
//加锁来计算x连续加2000次1

func addWithoutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}
//不加锁来计算x连续加2000次1

func Lock() {
   println("this is Lock")
   x := 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   println("addWithLock:", x)
   //执行5个协程,计算x+2000次1
   
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
  
   time.Sleep(time.Second)
   println("addWithoutLock:", x)
   //同上
}

//以上代码目的为:同上对x做5次连续加2000个1,结果应为10000

结果如图 image.png

加锁后执行得到了正确结果,不加锁的因读脏数据而导致结果不正确,比10000小

WaitGroup

sync.WaitGroup用于并发操作中等待一组Goroutine的返回。首先需要告诉程序有几个并发操作sync.WaitGroup.Add(x),在结束某个协程后,可用sync.WaitGroup.Done()来告诉程序结束了一个协程,最后利用sync.WaitGroup.Wait来阻塞,直到所有协程结束后再释放。

归根结底,WaitGroup就是一个计数器,当add了x个协程后,计数器就由0增加到x。每次Done()一个,就相当于Add(-1)。Wait()是看计数器最后为0时释放,否则阻塞。

以下为代码样例

func waitHello(i int) {
   println("hello Parallel : " + fmt.Sprint(i))
}

func WaitHello() {
   println("this is waitGrop")
   var wg sync.WaitGroup
   //要并行执行5个协程,所以add(5)
   wg.Add(5)
   //for'循环5个协程
   for i := 0; i < 5; i++ {
      go func(j int) {
         //执行完一个协程后通过defer来done
         defer wg.Done()
         waitHello(j)
      }(i)
   }
   //阻塞并等待5个协程都结束才释放
   wg.Wait()
}