【Go语言快速上手】第二部分:Go语言进阶之并发编程

Go 语言的并发编程是其一大亮点。通过 goroutinechannel,Go 使得并发编程变得简洁且高效。Go 还提供了 sync 包,包含了多种同步原语,帮助开发者在并发程序中处理共享数据和同步问题。

1. goroutine:创建和调度 goroutine

1.1 定义

Goroutine 是 Go 语言的核心并发机制,它是由 Go 运行时(runtime)管理的用户级线程。与操作系统级线程不同,Goroutine 是非常轻量级的。每个 Goroutine 会在栈上分配非常小的空间(通常为 2 KB),并且能够动态扩展栈的大小,这使得它们非常高效。

特点:

  • 轻量级: Goroutine 占用很小的内存和资源。你可以创建成千上万个 Goroutine。
  • 调度由 Go 运行时管理: Go 的运行时通过调度器自动分配和调度 Goroutine,通常一个操作系统线程可以运行多个 Goroutine。
  • 协作式调度: Goroutine 通过 Go 的调度器在多个 CPU 核心上并发执行,不需要开发者显式地管理线程。

优势:

  • 性能高效: 与操作系统线程相比,Goroutine 的创建、销毁和调度开销要低得多,能够支持大量的并发操作。
  • 简洁的语法: Goroutine 的使用非常简单,只需要在函数调用前加上 go 关键字。
  • 良好的并发模型: Go 语言通过协作式调度和通道机制,使得并发编程变得更加简单和直观。

1.2 Goroutine 的创建与使用

对于Go语言,直接通过 go 关键字即可将一个函数调用变为异步执行的 Goroutine。

go functionName(arguments)

go 关键字用于函数调用时,Go 会在一个新的 Goroutine 中并发执行该函数,而当前的 Goroutine(通常是主 Goroutine)会继续执行剩下的代码。

示例:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    
    
    fmt.Println("Hello from Goroutine!")
}

func main() {
    
    
    // 启动一个新的 Goroutine
    go sayHello()

    // 主 Goroutine 等待 1 秒,确保 Goroutine 执行完成
    time.Sleep(1 * time.Second)
    fmt.Println("Hello from main Goroutine!")
}

在这里插入图片描述

对于上面的代码,sayHello 函数会在新的 Goroutine 中并发执行,而 main 函数继续执行后面的代码。由于主 Goroutine 在退出之前通过 time.Sleep 等待了 1 秒,确保了新启动的 Goroutine 可以完成其任务。

1.3 Goroutine 的调度与并发

Go 语言的调度器会自动在多个操作系统线程中调度 Goroutine。当一个 Goroutine 执行时,它可能会被挂起,Go 调度器会选择其他 Goroutine 执行,直到当前的 Goroutine 被唤醒。这是基于协作式调度的。

Go 的运行时通过 M(机器,代表操作系统线程)、P(处理器,代表 Goroutine 运行的上下文)和 G(代表 Goroutine)的模型来实现 Goroutine 的调度。

1.4 Goroutine 的并发执行

虽然 Goroutine 在并发环境中执行,Go 语言的并发模型是非阻塞的。多个 Goroutine 通过调度器在多个 CPU 核心上并行运行。Go 的并发模型是通过基于消息传递的通信来协作的。

多个 Goroutine 并发执行

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    
    
    for i := 1; i <= 5; i++ {
    
    
        fmt.Println(i)
        time.Sleep(100 * time.Millisecond)
    }
}

func printLetters() {
    
    
    for _, letter := range "ABCDE" {
    
    
        fmt.Println(string(letter))
        time.Sleep(200 * time.Millisecond)
    }
}

func main() {
    
    
    // 启动两个 Goroutine
    go printNumbers()
    go printLetters()

    // 等待 Goroutine 完成
    time.Sleep(2 * time.Second)
}

输出(顺序不固定,因并发执行)

1
A
2
B
3
C
4
D
5
E

在这个例子中,printNumbersprintLetters 两个函数通过 go 关键字分别在不同的 Goroutine 中并发执行。它们的输出交替显示,因为它们是并发执行的。

1.5 Goroutine 并发示例

使用 sync.WaitGroup 等待多个 Goroutine 完成

在实际应用中,我们通常需要等待多个 Goroutine 完成任务。这时可以使用 sync.WaitGroup 来管理 Goroutine 的同步。

func task(id int, wg *sync.WaitGroup) {
    
    
    defer wg.Done() // 标记任务完成
    fmt.Printf("Task %d started\n", id)
    // 模拟任务的执行
}

func main() {
    
    
    var wg sync.WaitGroup

    // 启动 3 个 Goroutine
    for i := 1; i <= 3; i++ {
    
    
        wg.Add(1) // 增加一个等待计数
        go task(i, &wg)
    }

    // 等待所有 Goroutine 完成
    wg.Wait()

    fmt.Println("All tasks are completed!")
}

输出:

Task 1 started
Task 2 started
Task 3 started
All tasks are completed!

在这个例子中,sync.WaitGroup 用于等待所有的 Goroutine 完成。每次启动一个新的 Goroutine,都通过 wg.Add(1) 增加计数,Goroutine 执行完成后调用 wg.Done(),最后在 main 函数中通过 wg.Wait() 等待所有任务完成。


2. channel:无缓冲 channel、有缓冲 channel、select 语句

channel 是 Go 语言用于不同 goroutine 之间通信的机制。通过 channel,可以安全地传递数据,避免了数据竞争问题。Go 提供了无缓冲和有缓冲的 channel,以及 select 语句来处理多个 channel 的操作。

2.1 无缓冲 channel

  1. 无缓冲 channel 是最简单的一种 channel 类型,它没有任何存储空间。发送方和接收方必须在同一时刻进行操作,才能进行数据传递。
  2. 也就是说,发送数据的 goroutine 会阻塞,直到接收方 goroutine 从 channel 中接收到数据;同样,接收数据的 goroutine 也会阻塞,直到有数据被发送到该 channel。

即无缓冲 channel 会在发送数据和接收数据时进行同步,确保发送和接收操作相互配合。

特点:

  • 发送和接收操作是同步的。
  • 不支持缓冲,因此无法存储数据,必须有接收方在发送方发送数据时才会继续执行。
package main

import "fmt"

func main() {
    
    
    ch := make(chan int) // 创建一个无缓冲 channel

    // 启动一个 goroutine 发送数据
    go func() {
    
    
        ch <- 42 // 发送数据到 channel
        fmt.Println("Sent 42 to channel")
    }()

    // 接收数据
    value := <-ch
    fmt.Println("Received:", value)
}

输出:

Sent 42 to channel
Received: 42

对于上面的代码中,发送操作和接收操作是同步进行的。

2.2 有缓冲 channel

  1. 有缓冲 channel 与无缓冲 channel 不同,它在内部有一个缓冲区,用来存储一定数量的元素。
  2. 发送方在数据被发送到 channel 后并不一定阻塞,直到缓冲区满;
  3. 接收方也不必在发送方发送数据时立即接收,可以在缓冲区中有数据时异步处理。

总结:有缓冲的 channel 可以在发送数据时不必立即等待接收方,直到缓冲区满或接收方取走数据。创建有缓冲 channel 时,可以指定缓冲区的大小。

特点:

  • 可以缓存一定数量的数据。
  • 当缓冲区满时,发送操作会阻塞,直到有空间。
  • 当缓冲区为空时,接收操作会阻塞,直到有数据。
package main

import "fmt"

func main() {
    
    
    ch := make(chan int, 3) // 创建一个有缓冲区大小为 3 的 channel

    // 启动多个 goroutine 向 channel 发送数据
    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println("Sent 3 values to channel")

    // 启动一个 goroutine 接收数据
    fmt.Println(<-ch) // 接收并打印数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

输出:

Sent 3 values to channel
1
2
3

在上面的代码中,由于有缓冲区,所以可以在没有接收方的情况下先发送数据。

2.3 select 语句

  1. select 语句是 Go 中处理多个 channel 的一个非常强大的特性。
  2. 它允许一个 goroutine 等待多个 channel 操作,并且可以根据哪个 channel 准备好(即哪个操作不会阻塞)来执行相应的代码。
  3. select 语句类似于 switch 语句,但它是基于 channel 的异步操作。

总结:select 语句允许你等待多个 channel 操作。类似于 switch 语句,但用于处理多个 channel 的发送或接收。

特点:

  • select 会随机选择一个可以执行的 case,如果有多个 case 都准备好,Go 会随机选择一个执行。
  • 如果没有 channel 操作可执行,select 会阻塞,直到其中一个 channel 准备好。
  • 可以同时监听多个 channel。
  • 可以结合 time.After() 进行超时控制。
  • 可以通过 default 分支来避免阻塞。
package main

import (
    "fmt"
    "time"
)

func main() {
    
    
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
    
    
        time.Sleep(1 * time.Second)
        ch1 <- "Message from ch1"
    }()

    go func() {
    
    
        time.Sleep(2 * time.Second)
        ch2 <- "Message from ch2"
    }()

    // 使用 select 语句监听多个 channel
    select {
    
    
    case msg1 := <-ch1:
        fmt.Println("Received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received:", msg2)
    }
}

对于上面的代码,select 会等待 ch1ch2 中的任一 channel 可用。当第一个 channel 可用时,它会立即执行对应的 case,并停止等待。

在这里插入图片描述


3. sync 包:Mutex、RWMutex、WaitGroup 等同步原语

Go 提供了 sync 包中的多种同步原语,用于处理并发程序中的共享资源访问问题。

3.1 Mutex:互斥锁

Mutex 是最常用的同步原语,它确保同一时刻只有一个 goroutine 可以访问共享资源。通过 LockUnlock 方法来加锁和解锁。

package main

import (
    "fmt"
    "sync"
)

var counter int
var mutex sync.Mutex

func increment() {
    
    
    mutex.Lock() // 加锁
    counter++
    mutex.Unlock() // 解锁
}

func main() {
    
    
    var wg sync.WaitGroup

    // 启动多个 goroutine 进行并发操作
    for i := 0; i < 1000; i++ {
    
    
        wg.Add(1)
        go func() {
    
    
            defer wg.Done()
            increment()
        }()
    }

    wg.Wait() // 等待所有 goroutine 执行完毕
    fmt.Println("Final counter:", counter)
}

在这个示例中,使用 Mutex 来保证对 counter 的并发访问是安全的。

在这里插入图片描述

3.2 RWMutex:读写互斥锁

RWMutex 是一种读写互斥锁,它允许多个 goroutine 同时读取共享资源,但在写操作时会阻止其他的读写操作。

package main

import (
    "fmt"
    "sync"
)

var counter int
var rwMutex sync.RWMutex

func read() int {
    
    
    rwMutex.RLock() // 读锁
    defer rwMutex.RUnlock()
    return counter
}

func write() {
    
    
    rwMutex.Lock() // 写锁
    counter++
    rwMutex.Unlock()
}

func main() {
    
    
    var wg sync.WaitGroup

    // 启动多个 goroutine 进行读写操作
    for i := 0; i < 1000; i++ {
    
    
        wg.Add(1)
        go func() {
    
    
            defer wg.Done()
            write()
        }()
    }

    for i := 0; i < 1000; i++ {
    
    
        wg.Add(1)
        go func() {
    
    
            defer wg.Done()
            read()
        }()
    }

    wg.Wait()
    fmt.Println("Final counter:", counter)
}

在这个示例中,RWMutex 允许多个 goroutine 同时进行读操作,但在执行写操作时会锁住资源。

3.3 WaitGroup:等待多个 goroutine 完成

WaitGroup 用于等待一组 goroutine 执行完毕。它提供了 AddDoneWait 方法来控制并发流程。

package main

import (
    "fmt"
    "sync"
)

func task(wg *sync.WaitGroup) {
    
    
    defer wg.Done() // 执行完毕时调用 Done
    fmt.Println("Task completed")
}

func main() {
    
    
    var wg sync.WaitGroup

    // 启动多个 goroutine
    for i := 0; i < 5; i++ {
    
    
        wg.Add(1) // 增加等待的 goroutine 数量
        go task(&wg)
    }

    wg.Wait() // 等待所有 goroutine 执行完毕
    fmt.Println("All tasks completed")
}

在这个示例中,WaitGroup 用于等待多个并发任务完成。

猜你喜欢

转载自blog.csdn.net/Dreaming_TI/article/details/145674640