Go 语言的并发

Go 语言中的多线程操作是其语言的一大特色,它具有其它语言无法比拟的,可以近乎无限开启的线程。在 Go 语言中被称之为 goroutine ,它是线程的轻量级实现。Go 语言的并发广泛的应用在服务器性能调优的场景中,这也是越来越多的游戏服务器开发都在往 Go 语言倾斜的原因之一。

 

1.Go 语言的 goroutine

在 Go 语言中使用 go 关键字来创建 goroutine ,形如go 函数名()的形式去创建。每一个 goroutine 必须是一个函数,这个函数也可以是匿名函数

代码示例:

代码块
  • package main
  • 2
  • import (
  • 4        "fmt"
  • 5        "time"
  • )
  • 7
  • func main() {
  • 9        //打印0到10的数字
  • 10      go print0to10()
  • 11      //打印A到Z的字符
  • 12      go func() {
  • 13              for i := 'A'; i <= 'K'; i++ {
  • 14                      fmt.Println("printAtoK:", string(i))
  • 15                      time.Sleep(time.Microsecond)
  • 16              }
  • 17        }()
  • 18        time.Sleep(time.Second)
  • 19  }
  • 20
  • 21  func print0to10() {
  • 22            for i := 0; i <= 10; i++ {
  • 23                    fmt.Println("print0to10:", i)
  • 24                    time.Sleep(time.Microsecond)
  • 25            }
  • 26  }
  • 第 10 行:创建一个打印0到10数字的函数的 goroutine;
  • 第 11 行:使用匿名函数的方式创建一个打印A到Z的字符的 goroutine;
  • 第 15 和第 24 行:运行等待,让出执行资源给其它 goroutine;
  • 第 18 行:main 函数也是一个 goroutine,在它执行结束后系统会杀掉在这个 goroutine 中执行的所有goroutine ,所以要在 main 函数中加一个等待,为其内部的 goroutine 留出执行时间。

执行结果:

图片描述

从执行结果中可以看出打印数字和打印字符的两个 goroutine 是并发执行的。执行顺序是由 cpu 来调度的,所以执行结果可能每次都不一样。

 

2. Go语言并发通讯

其它语言并发时进程中的通讯一般都是通过共享内存(全局变量)的方式来实现的,这样一来各个模块之间的耦合会变得非常紧密。所以后来提出了使用通讯来共享内存这一概念,来解耦合。在 Go 语言中就是使用 channel 的方式来达到这一目的的。

代码示例:

代码块
  • package main
  • 2
  • import (
  • 4          "fmt"
  • 5          "time"
  • )
  • 7
  • var c1 chan rune = make(chan rune, 0)
  • var c2 chan int = make(chan int, 0)
  • 10
  • 11  func main() {
  • 12          //打印0到10的数字
  • 13          go print0to10()
  • 14          //打印A到Z的字符
  • 15           go func() {
  • 16                    c2 <- 0
  • 17                    for i := 1; i <= 11; i++ {
  • 18                            char := <-c1
  • 19                            fmt.Println("printAtoK:", string(char))
  • 20                            c2 <- i
  • 21                    }
  • 22            }()
  • 23            time.Sleep(time.Second)
  • 24  }
  • 25
  • 26  func print0to10() {
  • 27            for i := 'A'; i <= 'K'; i++ {
  • 28                   num := <-c2
  • 29                   fmt.Println("print0to10:", num)
  • 30                   c1 <- i
  • 31            }
  • 32  }

上述代码主要实现的功能为,使用两个通道来使两个 goroutine 互相通讯,从而使得它们的打印安装轮流打印的方式打印数字和字母。

  • 第 8 行:实例化一个字符通道用于接收字符;
  • 第 9 行:实例化一个数字通道用于接收数字;
  • 第 16 行:向数字通道中塞入数字0,用于触发打印数字的 goroutine;
  • 第 18 行:从字符通道中获取一个待打印的字符。若通道中无字符,则阻塞等待;
  • 第 20 行:字符打印完毕之后再向数字通道中塞入后续数字,触发打印数字的 goroutine;
  • 第 28 行:从数字通道中获取待打印的数字,若通道中无数字,则阻塞等待;
  • 第 30 行:数字打印完毕之后再向字符通道中塞入后续字符,触发打印字符的 goroutine。

执行结果:

图片描述

和没用使用 channel 之前的代码不同,这次等同于使用 channel 实现了 goroutine 的调度,使其轮流执行。

 

3. Go语言进程锁

在之前介绍 map 的小节中提到过线程不安全的 map 。之所以线程不安全是因为其内部实现机制中无法同时读写,若有两个 goroutine 一个在读取 map 中的值,而另一个在更新 map 中的值,就会导致程序崩溃。

代码示例:

代码块
  • package main
  • 2
  • import (
  • 4            "fmt"
  • 5            "time"
  • )
  • 7
  • func main() {
  • 9            m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
  • 10          //创建100个goroutine对map进行读写
  • 11          for i := 0; i < 100; i++ {
  • 12                    go func() {
  • 13                              for v := range m {
  • 14                                      m[v] = 100
  • 15                              }
  • 16                    }()
  • 17          }
  • 18          time.Sleep(time.Second)
  • 19          fmt.Println(m)
  • 20  }

执行上述代码有时会输出正确结果:

图片描述

但更多的时候会输出读写冲突的错误:

图片描述

这个就是线程不安全的 map 不建议使用的原因,除了直接使用线程安全的 map 之外,还可以为这些 goruntine 加上锁,使其无法同时对 map 进行读写操作,这样也可以保障各线程的安全。

代码示例:

代码块
  • package main
  • 2
  • import (
  • 4            "fmt"
  • 5            "sync"
  • 6            "time"
  • )
  • 8
  • func main() {
  • 10          var lock sync.Mutex//定义一个锁变量
  • 11          m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
  • 12          for i := 0; i < 100; i++ {
  • 13                  go func() {
  • 14                          lock.Lock()//在读取map前锁定这个锁,使其它线程访问这个锁要阻塞
  • 15                          for v := range m {
  • 16                                  m[v] = 100
  • 17                          }
  • 18                          lock.Unlock()//在读取map前释放这个锁
  • 19                  }()
  • 20          }
  • 21          time.Sleep(time.Second)
  • 22          fmt.Println(m)
  • 23  }

加了锁之后,你就会发现无论执行几次,执行结果都是正确的。

图片描述

 

4. 小结

本文主要介绍了Go语言中的多线程——goroutine。其实现是线程的轻量实现,所以可以无限制的开启。在使用过程中需要注意:

  • goroutine 执行无先后顺序,由 cpu 统一调度。
  • goroutine 之间内存的共享通过使用 channel 来通讯实现。
  • goroutine 使用线程不安全的变量类型时可以用锁将其锁定。

文章来源于网络,侵删!

相关新闻

历经多年发展,已成为国内好评如潮的Linux云计算运维、SRE、Devops、网络安全、云原生、Go、Python开发专业人才培训机构!