Go语言进阶 | 豆包MarsCode AI 刷题

82 阅读4分钟

1. 语言进阶

并发与并行

  • 并发:多线程程序在一个核心的CPU上运行,通过上下文切换(Context Switching)实现多个任务交替执行。并发的核心理念是“交替执行”,即系统在一个处理器上轮流运行多个任务,给人一种同时进行的感觉。
  • 并行:多个线程程序在多个核心的CPU上并行执行,每个线程在独立的核心上运行,可以同时进行多个任务。并行的核心理念是“真正同时进行”,可以充分利用多核CPU的计算能力。

Go的并发编程优势:Go语言原生支持并发编程,通过Goroutine和Channel等机制,可以高效利用多核CPU,提升程序的性能和响应能力。

1.1 Goroutine(协程)

  • 协程:协程是用户态的轻量级线程,具有非常小的栈空间(通常是KB级别),创建和销毁开销低,可以在同一进程中并发运行多个协程。Goroutine的调度由Go运行时(runtime)负责,而不是操作系统。每个Goroutine会在一个或多个系统线程上运行,由Go调度器进行管理。
  • 线程:线程是操作系统级别的线程,通常是内核态的。线程需要较大的栈空间(通常是MB级别),因此创建和销毁线程的开销相对较高。一个线程可以承载多个协程。

Goroutine的优势

  • 高效:每个Goroutine的栈空间可以动态增长和缩小,最大可以减少内存的浪费。
  • 并发性:Go的调度器会根据需要将Goroutine分配到系统线程上,使得程序可以在多核CPU上并行执行多个Goroutine。

使用Goroutine的案例: 需求:快速打印 "hello goroutine: 0" 到 "hello goroutine: 4"。

package main

import (
	"fmt"
	"time"
)

func hello(i int) {
    fmt.Println("hello goroutine : " + fmt.Sprint(i))
}

func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second) // 等待足够的时间让Goroutine执行
}

func main() {
    HelloGoRoutine()
}

解释

  • 使用 go 关键字来启动一个新的Goroutine。在每次循环中,我们都启动一个新的协程来调用 hello(i)
  • time.Sleep(time.Second) 用来确保所有Goroutine有足够的时间输出结果,因为Goroutine是异步执行的。

1.2 CSP(通信顺序进程)

  • CSP (Communicating Sequential Processes) 是Go并发编程中的核心思想。CSP提倡通过通信来共享内存,而不是通过共享内存来实现通信
  • 通过通信共享内存:每个进程有独立的内存空间,进程之间通过消息传递来共享数据。Go语言中的 Channel 就是这种通信机制的实现。
  • 通过共享内存实现通信:传统的多线程编程往往通过共享内存(如全局变量)来进行数据传递,但这种方法需要处理同步问题,如加锁、竞态条件等,容易引发性能问题。Go鼓励通过消息传递来避免这种复杂性。

实践中,避免共享内存的并发编程模型,可以降低程序的复杂性,并且提升性能。

1.3 Channel(通道)

Channel 是Go语言中的一个核心特性,它可以在不同的Goroutine之间传递数据。Channel 提供了一种安全的、同步的通信方式,保证了数据的传输不会发生竞态条件。

  • 无缓冲通道:没有指定缓冲区大小的通道,发送者和接收者需要同步进行,即发送操作会阻塞,直到有接收者准备好接收数据;接收操作会阻塞,直到有数据发送进来。

    ch := make(chan int) // 无缓冲通道
    
  • 有缓冲通道:指定了缓冲区大小的通道,发送者可以在缓冲区未满的情况下发送数据,而接收者可以在缓冲区不为空时接收数据。发送者和接收者不必同步进行,因此这种通道也称为“异步通道”。

    ch := make(chan int, 2) // 有缓冲区的通道,最多可以存储2个元素
    

案例: 需求:

  • A 子协程发送 0 到 9 的数字。
  • B 子协程计算输入数字的平方。
  • 主协程输出最后的平方数。
package main

import (
	"fmt"
)

func main() {
    // 创建两个通道
    ch1 := make(chan int) // 用于发送数字
    ch2 := make(chan int) // 用于发送平方数

    // 子协程A: 发送0-9数字
    go func() {
        for i := 0; i < 10; i++ {
            ch1 <- i
        }
        close(ch1) // 关闭通道,表示发送完成
    }()

    // 子协程B: 计算数字的平方
    go func() {
        for num := range ch1 { // 从通道接收数字
            ch2 <- num * num
        }
        close(ch2) // 关闭通道,表示计算完成
    }()

    // 主协程: 输出结果
    for square := range ch2 { // 从通道接收平方数
        fmt.Println(square)
    }
}

解释

  • A 子协程通过 ch1 通道发送数字,B 子协程从 ch1 接收数字并计算平方后,通过 ch2 通道发送结果。
  • 主协程从 ch2 接收平方结果并打印输出。
  • 这里使用了 range 循环来自动接收直到通道关闭。