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循环来自动接收直到通道关闭。