Go并发编程的基本使用|青训营笔记

65 阅读4分钟

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

进程、线程、协程

  1. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

  2. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。 A. 多线程程序在一个核的cpu上运行,就是并发。 B. 多线程程序在多个核的cpu上运行,就是并行

  3. 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程。一个线程上可以跑多个协程,协程是轻量级的线程。 goroutine 只是由官方实现的超级"线程池":每个实例4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因

  4. 并发与并行的区别: 并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。

Goroutine

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴

常用方法

// 并发执行一个协程
go func()
// WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
var wg = sync.WaitGroup{} // 创建wg变量
wg.Add(1) // 每开启一个线程,让wg自增1
wg.Done() // Done方法减少WaitGroup计数器的值,应在线程的最后执行
wg.Wait() // Wait方法阻塞直到WaitGroup计数器减为0

Channel

  • Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
  • 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

chan类型声明与使用

var 变量 chan 元素类型
var ch1 chan int // 声明一个传递int数据的chan
var ch2 chan bool // 声明一个传递bool数据的chan
var ch3 chan []int // 声明一个传递int切片的chan

声明的通道后需要使用make函数初始化之后才能使用。

chan操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。 发送和接收都使用<-符号,关闭通道使用内置函数close()

ch <- 10 // 把10发送到ch中
x := <- ch // 从ch中接收值并赋值给变量x
close(ch) // 关闭通道

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

如何优雅的从通道循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

  1. chan<- int是一个只能发送的通道,可以发送但是不能接收;
  2. <-chan int是一个只能接收的通道,可以接收但是不能发送。