37. Go 语言的并发

目录
文章目录隐藏
  1. 1. Go 语言的 goroutine
  2. 2. Go 语言并发通讯
  3. 3. Go 语言进程锁
  4. 4. 小结

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

1. Go 语言的 goroutine

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

代码示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    //打印 0 到 10 的数字
    go print0to10()
    //打印 A 到 Z 的字符
    go func() {
        for i := 'A'; i <= 'K'; i++ {
            fmt.Println("printAtoK:", string(i))
            time.Sleep(time.Microsecond)
        }
    }()
    time.Sleep(time.Second)
}

func print0to10() {
    for i := 0; i <= 10; i++ {
        fmt.Println("print0to10:", i)
        time.Sleep(time.Microsecond)
    }
}
  • 第 10 行:创建一个打印 0 到 10 数字的函数的 goroutine;
  • 第 11 行:使用匿名函数的方式创建一个打印 A 到 Z 的字符的 goroutine;
  • 第 15 和第 24 行:运行等待,让出执行资源给其它 goroutine;
  • 第 18 行:main 函数也是一个 goroutine,在它执行结束后系统会杀掉在这个 goroutine 中执行的所有 goroutine ,所以要在 main 函数中加一个等待,为其内部的 goroutine 留出执行时间。

执行结果:

Go 语言的 goroutine

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

2. Go 语言并发通讯

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

代码示例:

package main

import (
    "fmt"
    "time"
)

var c1 chan rune = make(chan rune, 0)
var c2 chan int = make(chan int, 0)

func main() {
    //打印 0 到 10 的数字
    go print0to10()
    //打印 A 到 Z 的字符
    go func() {
        c2 <- 0
        for i := 1; i <= 11; i++ {
            char := <-c1
            fmt.Println("printAtoK:", string(char))
            c2 <- i
        }
    }()
    time.Sleep(time.Second)
}

func print0to10() {
    for i := 'A'; i <= 'K'; i++ {
        num := <-c2
        fmt.Println("print0to10:", num)
        c1 <- i
    }
}

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

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

执行结果:

Go 语言并发通讯

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

3. Go 语言进程锁

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

代码示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
    //创建 100 个 goroutine 对 map 进行读写
    for i := 0; i < 100; i++ {
        go func() {
            for v := range m {
                m[v] = 100
            }
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(m)
}

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

输出正确结果

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

Go 语言进程锁

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

代码示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var lock sync.Mutex//定义一个锁变量
    m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
    for i := 0; i < 100; i++ {
        go func() {
            lock.Lock()//在读取 map 前锁定这个锁,使其它线程访问这个锁要阻塞
            for v := range m {
                m[v] = 100
            }
            lock.Unlock()//在读取 map 前释放这个锁
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(m)
}

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

Go 语言进程锁

4. 小结

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

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

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系maynote@foxmail.com处理
码云笔记 » 37. Go 语言的并发

发表回复