编程初学者的Go语言学习之旅 | 青训营笔记

83 阅读4分钟

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

前言

本篇其实是在写昨天的内容,Go语言的并发编程

Go语言进阶

并发与并行

简单来说,并行真正意义上的,同一个时间段内可以做不同的事。
而并发是通过某种调度方法,交替地做不同的事。 二者均可以提升执行效率,但并行使用的线程(Thread)在内存资源占用上要比并发使用的协程(Coroutine)高很多,使用并发更具有性价比。

Goroutine

代码层面上,Go使用go func即可创建一个协程,下面是一个简单的小程序,使用Go的并发编程打印Hello0-4:

//1.1
package main  
import (  
   "fmt"  
   "time")  
  
func PrintHello(i int) {  
   fmt.Println("Hello" + fmt.Sprint(i))  
}  
func main() {  
   for i := 0; i < 5; i++ {  
      go func(m int) {  
         PrintHello(m)  
      }(i)  
   }  
   time.Sleep(time.Second)  
}

输出结果为:

Hello2
Hello1
Hello0
Hello4
Hello3

对上面的代码稍作解释: 1.func(){ //代码块 }() 这是一种匿名函数的声明方式,位数}尾的( )表示传入实参并调用。

2.time.second 调用time包里的Second方法,其返回值为1s。

3.time.Sleep(T) 使当前协程休眠T s,它的作用我们稍后解答。

实际上,这个程序里创建了6个协程。虽然没有以go func的形式声明,但此处的func main也为一个协程,并被称为主协程。其余的5个go func均为子协程。

那么说回time.Sleep方法,看看如果我们注释掉这一行会发生什么:

没有打印出任何东西,为什么?

实际上在Go中,主协程结束时(此处的func main),其子协程也会停止运行。由于所有的PrintHello()都是子协程调用的,所以程序没来得及打印出任何东西就结束了。 故time.Sleep()在此处的作用是让主协程“等”子协程1s,待子协程运行完成时,再结束主协程。

但是,在实际的编程中,我们一般情况下无法得知子协程运行所需要的时间,使用time.Sleep(T)极可能使其休眠过长或者过短,造成运行速度下降或者主协程结束过快的问题,因此,我们需要用到WaitGroup。

等待组(WaitGroup)

WaitGroup是sync包下的一种类型,需要用impo导入后再使用。我们来看看经由WaitGroup改造后的程序1.1:

//1.2
package main  
  
import (  
   "fmt"  
   "sync")  
  
var Clock sync.WaitGroup  
  
func PrintHello(i int) {  
   fmt.Println("Hello" + fmt.Sprint(i))  
}  
func main() {  
   Clock.Add(5)  
   for i := 0; i < 5; i++ {  
      go func(m int) {  
         defer Clock.Done()  
         PrintHello(m)  
      }(i)  
   }  
   Clock.Wait()  
}

输出结果仍为:

Hello2
Hello1
Hello0
Hello4
Hello3

稍微解释一下这些改造: 1.var Clock sync.WaitGroup 定义一个名为Clock的sync.WaitGroup类型

2.Clock.Add(T) 调用WaitGroup的Add方法,使WaitGroup的计数器+T(T可为负数)

3.defer Clock.Done() 调用WaitGroup的Done方法,使WaitGroup的计数器-1

4.Clock.Wait() 调用sync.WaitGroup的Wait方法阻塞直到WaitGroup计数器减为0。

5.defer关键字 使代码在函数末尾进行。(假设C语言中有defer关键字,那么你可以malloc()后马上写defer free(),以避免忘记释放内存报错,还是挺不错的,可惜没有)

此程序有一个主协程五个子协程,用Add方法使WaitGroup计数器+5,每结束一个子协程,Done方法会使计数器-1,在减为0之前,Wait方法都会使协线程阻塞,以达到在所有子协程结束之前主协程都不会结束的效果,并且不会像time.Sleep方法一样出现运行速度下降或者主协程结束过快的问题。

CSP(Communicating Sequential Processes)并发模型

如何在不同的协程中实现通信?Go语言中提供了两种方法:通过共享内存实现通信与通过通信实现共享内存。Go更推荐第二种,我们拿出来细说。

通过通信实现共享内存。

Channel可以实现协程与协程之间的数据通信。

ChannelName :=make(chan type,capacity)
//capacity为缓冲区,若不需要则省略

使用Channel发送/接受数据:

V := <- ChannelName
ChannelName <- V

for range同样可以像遍历数组、map一样遍历Channel读取数据

for v := range ChannelName {
    fmt.Println(v)
}

使用close函数关闭Channel(当你不再需要使用Channel发送数据):

close(ChannelName)

我们可以用Channel设计一个程序:

// 1.4 
package main  
  
import "fmt"  
  
func main() {  
   out := make(chan int)  
   square := make(chan int)  
   go func() {  
      for i := 0; i < 10; i++ {  
         out <- i  
      }  
      close(out)  
   }()  //子协程1
   go func() {  
      for i := range out {  
         square <- i * i  
      }  
      close(square)  
   }()  //子协程2
   for i := range square {  
      fmt.Println(i)  
   }  
}

输出:

0
1 
4 
9 
16
25
36
49
64
81

子协程1传入int 1-9给Channel out

子协程2从Channel out接受数据,平方后传入Channel square

主协程从Channel square 接受数据并打印

引用

Go语言进阶与依赖管理

从 Java 的角度实践 Go 工程| 青训营笔记

分发

This work is licensed under CC BY-SA 4.0