Go的并发编程|青训营

86 阅读5分钟

并发

Gorountines

在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。

线程和协程

Screenshot_20230726_160846.png

Screenshot_20230726_161508.png

协程和线程的区别

协程(Coroutine)和线程(Thread)都是并发编程中用于实现并发执行的概念,但它们在实现方式和用途上有一些重要的区别。

  1. 调度方式:

    • 线程是由操作系统内核进行调度和管理的,称为内核线程。线程切换由操作系统完成,切换开销较大。
    • 协程是由编程语言的运行时环境(如Go语言的Go调度器)进行调度和管理的,称为用户态线程。协程切换由编程语言的运行时环境完成,切换开销较小。
  2. 并发性能:

    • 由于线程切换是由操作系统完成的,线程的创建、销毁和切换开销较大,因此创建大量线程可能导致系统负担过重。
    • 协程切换由编程语言运行时环境负责,切换开销较小,可以轻松创建大量的协程,提高并发性能。
  3. 内存占用:

    • 线程在创建时需要分配一定的堆栈空间,同时需要维护内核数据结构,因此线程的内存占用较大。
    • 协程的内存占用较小,因为它们共享相同的堆栈,无需维护内核数据结构。
  4. 多核利用:

    • 线程可以在多核处理器上并行执行,利用多核优势。
    • 一般情况下,单个协程只能在一个线程中运行,无法直接利用多核处理器,但在某些语言中,可以通过多线程和多协程的结合来实现多核利用。
  5. 同步方式:

    • 线程之间的同步通常通过共享内存和锁等机制实现,容易出现死锁和竞态条件。
    • 协程之间的同步通常通过通道(Channel)等机制实现,由于通道本身就是并发安全的,因此更容易编写线程安全的代码。

channel

make(chan 元素类型,[缓冲大小])

无缓冲通道 make(chan int)

有缓冲通道 make(chan int ,2) ‍

Screenshot_20230726_161848.png

案例:

func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3) //有3个空间的缓冲通道

    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()

    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()

    for i := range dest {
        println(i)
    }
}

defer​ 是 Go 语言中的一个关键字,用于延迟(defer)函数或方法的执行直到包含它的函数(或当前代码块)执行完毕。defer​ 语句允许您在函数退出之前执行一些清理或善后工作。

常见的 defer​ 用途包括:

  1. 关闭资源:例如文件、数据库连接或网络连接等,在函数结束前及时关闭它们。
  2. 解锁资源:在函数结束时释放被锁定的资源,以防止死锁。
  3. 记录日志:在函数退出前记录函数的状态、执行信息或错误日志。
  4. 清理资源:在函数结束前释放动态分配的内存或资源。

defer​ 的使用可以增强代码的可读性和健壮性,同时确保在函数退出时进行必要的清理工作。

上述代码开辟了src​和dest​两个channel,src​没有缓存,dest​有三个缓存空间,go func() {...}()​: 创建第一个匿名 goroutine,用于生成 0 到 9 的数字并将它们发送到 src​ 通道中。在这个例子中,由于 src​ 是无缓冲通道,这个 goroutine 会在数据发送后阻塞直到第二个 goroutine 开始接收数据。

在这个例子中,由于 dest​ 是带有3个空间的缓冲通道,它可以缓冲三个结果。所以,第一个 goroutine 在发送完 0 到 2 的平方结果后不会被阻塞。然后,第二个 goroutine 在接收这三个结果之前不会阻塞,因为它可以从缓冲通道中接收数据。当第二个 goroutine 开始发送数据时,它会阻塞,直到主 goroutine 继续接收数据。

WaitGroup

在Go语言中,sync.WaitGroup​ 是一个用于等待一组 goroutine 完成执行的工具。它通常用于在主goroutine等待所有其他goroutine执行完毕后再继续执行。WaitGroup​ 通过计数器来实现,通过 Add​ 方法增加计数器值,通过 Done​ 方法减少计数器值,通过 Wait​ 方法阻塞等待计数器值归零。

下面是一个简单的示例来说明如何使用 WaitGroup​:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 在函数结束时减少计数器值
    fmt.Printf("Worker %d starting\n", id)
    // 模拟一些工作
    for i := 0; i < 5; i++ {
        fmt.Printf("Worker %d: %d\n", id, i)
    }
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1) // 增加计数器值
        go worker(i, &wg)
    }
    wg.Wait() // 阻塞等待计数器值归零
    fmt.Println("All workers done")
}

在上面的示例中,我们创建了3个goroutine(worker),每个worker模拟一些工作。在每个worker函数中,我们通过 defer​ 关键字在函数退出时调用 Done()​ 方法,以减少 WaitGroup​ 的计数器值。然后,我们在主goroutine中调用 wg.Wait()​ 方法,它会一直阻塞直到 WaitGroup​ 的计数器值归零,即所有的worker都执行完毕。

请注意,WaitGroup​ 是通过指针传递给worker函数的,因为需要修改 WaitGroup​ 中的计数器值,保证在每个worker完成时正确减少计数器。

使用 WaitGroup​ 可以非常方便地实现等待一组goroutine执行完毕的功能,特别是在处理并发任务时。