Go并发编程快速入门 | 青训营笔记

50 阅读3分钟

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

协程

协程是轻量级的线程。Go语言很好地实现了协程,并且优化做得很好,一个线程上能轻松开启几万条协程。 Go协程的特点:

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级线程

进程、线程的调度都是由操作系统控制的,而协程则是完全由用户控制的,操作系统感知不到协程的存在。

快速入门

使用go关键字开启一个协程,协程以函数为单位。

  • 在主线程中,开启一个协程,每隔 1 秒输出 “hello world”,输出 30 次
  • 在主线程中也每隔 1 秒输出“hello golang”,输出 10 次后,退出程序
package main
​
import (
  "fmt"
  "strconv"
  "time"
)
​
// 每隔一秒输出 “hello world”,输出 30 次
func test() {
  for i := 0; i < 30; i++ {
    fmt.Println("test() ===> hello world! " + strconv.Itoa(i))
    // 间隔 1 秒
    time.Sleep(time.Second)
  }
}
​
func main() {
​
  //test()  // 顺序执行
  go test() // 开启协程for i := 0; i < 10; i++ {
    fmt.Println("main() ===> hello go! " + strconv.Itoa(i))
    // 间隔 1 秒
    time.Sleep(time.Second)
  }
}
​

执行流程:

  • 线程是一个物理线程,直接作用在 CPU 上,是重量级的,非常耗费 CPU 资源。
  • 协程由线程开启,是轻量级线程,是逻辑态,对资源消耗相对小。
  • Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般都是基于线程的,开启过多的线程,资源耗费大。由此体现 Go 语言在并发上的优势。

协程通信

Go 提倡通过通信共享内存而不是通过共享内存而实现通信。

  • 通过通信共享内存依赖通道(Channel)实现。
  • 通过共享内存实现通信即为加锁方式。
  • Go 提倡 Channel 方式,但也保留了加锁方式。

Channel

引用类型,通过make关键字创建:

make(chan 通道中元素的数据类型, [缓冲大小])
  • 如果不指定缓冲大小,则为无缓冲通道。无缓冲通道会使得发送的协程和接受的协程同步化。
  • 如果指定缓冲大小,则为有缓冲通道。当缓冲区满的时候会阻塞发送,即生产者消费者模式。
  • 带缓冲的 Channel 可以解决生产和消费速度不一致的问题。

实现生产者消费者模型:

func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3)
    // A 协程(匿名函数)
    go func() {
        defer close(src) // 延迟调用,在函数最后执行关闭释放资源
        for i := 0; i < 10; i++ {
            // 生产者协程,生产 0-9 数字,生产的数字放入通道
            src <- i
        }
    }()
    // B 协程
    go func() {
        defer close(dest)
        for i := range src {
            // 消费者协程,消费操作为将生产者协程发送过来的数据进行平方
            // 消费结果放入 dest 通道
            dest <- i * i
        }
    }()
    // 主线程从通道中输出最终的平方结果
    for i := range dest {
        fmt.Println(i)
    }
}

如果生产者的逻辑比较简单,消费者的逻辑比较复杂,那么生产者生产的速度比较快,消费者消费的速度比较慢,此时生产者和消费者之间如果用不带缓冲的 Channel 就会导致比较快的生产者要等待消费者消费,数据被消费后才能再进行生产。而使用带缓冲的 Channel 就可以解决这个速度不一致的问题。

Lock

Go 提倡通过通信实现共享内存,即 Channel 的机制,但 Go 也保留了通过共享内存实现通信,即 Lock 的机制。

var (
    x int64
    lock sync.Mutex // 锁
)
​
// 对变量执行 2000 次 +1 操作
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}
​
func Add() {
    x = 0
    for i := 0; i < 5; i++ {
        // 开启 5 个协程,操作共享数据(不加锁)
        go addWithoutLock()
    }
    // 等待 2s 让协程执行结束,再打印不加锁下的结果
    time.Sleep(time.Second * 2)
    fmt.Println("WithoutLock: ", x)
    x = 0
    for i := 0; i < 5; i++ {
        // 开启 5 个协程,操作共享数据(加锁)
        go addWithLock()
    }
    // 等待 2s 让协程执行结束,再打印加锁下的结果
    time.Sleep(time.Second * 2)
    fmt.Println("WithLock: ", x)
}

等待某个协程执行完成——并发任务的同步

sync.WaitGroup下会维护一个计数器来实现。其中的方法:

  • Add(delta int):计数器加delta
  • Done():计数器减一
  • Wait():阻塞直到计数器为0
func ManyGoWait() {
    var wg sync.WaitGroup
    // 开启五个协程
    wg.Add(5)
    for i := 0; i < 5; i++ {
        // 每个协程各打印0,1,2,3,4中的一个值
        go func(j int) {
            defer wg.Done() // 协程执行完毕的时候计数器减一
            fmt.Println(j)
        }(i)
    }
    // 主线程等待五个协程都执行完再继续往下执行
    wg.Wait()
    fmt.Println("五个协程都执行完毕.")
    // do some ...
}