39、Go语言基础 - 并发编程 - goroutine(协程)
hi,我是温新
并发编程基础
并发与并行
并发:同一时间段内执行多个任务;
并行:同一时刻执行多个任务。
下面通过案例来解释:
并行(Parallelism):
在并行的情况下,银行拥有多个独立的柜台,每个柜台由一个银行职员处理一个客户的事务。这意味着多个事务可以同时进行,因为每个柜台都有自己的处理能力。客户1可以在柜台1办理业务,同时客户2可以在柜台2办理业务,这两个事务是真正同时执行的,不互相干扰。
这是真正的并行处理,就像在多核 CPU 上运行多个任务一样。并行处理提高了整体的吞吐量,因为多个事务可以同时进行。
并发(Concurrency): 在并发的情况下,银行只有一个柜台,但有多个客户排队等待办理业务。柜台的银行职员在不同的客户之间快速切换,每个客户都得到一些时间来处理他们的业务,然后移到下一个客户。这是因为柜台的银行职员在不同的客户之间交替工作,以最大限度地减少等待时间。
虽然柜台只有一个,但多个客户的业务在时间上交替执行,这就是并发。并发处理通常用于提高资源利用率,因为在任何给定时刻只有一个客户被服务,但它们可以共享柜台和银行职员的资源
总结:
- 并行是多个任务真正同时执行,通常需要多个处理单元。
- 并发是多个任务在时间上交替执行,通过快速切换来模拟同时执行的效果,可以在单核或多核环境中使用。
进程、线和、协程
-
进程(Process):
- 进程是操作系统中的一个独立执行单元,拥有自己的内存空间、系统资源和运行环境。
- 每个进程都有独立的地址空间,相互隔离,通常需要通过进程间通信(IPC)来进行数据交换。
- 进程通常用于执行独立的程序,如应用程序或服务,每个进程都有自己的进程号(PID)。
- 进程切换需要较多的系统开销,通常较为重量级。
-
线程(Thread):
- 线程是进程内的执行单元,共享相同的内存空间和资源。
- 线程可以看作是轻量级的进程,多个线程可以并发执行,它们之间可以更容易地共享数据。
- 线程通常用于实现多线程编程,提高程序的并发性能,但需要注意处理线程间的同步和互斥问题。
- 线程通常属于同一进程,因此不需要像进程之间那样的IPC。
-
协程(Goroutine 或 Coroutine):
- 协程是一种轻量级的线程,它可以在用户空间实现并发,而不需要像线程那样由操作系统进行管理。
- 协程可以被认为是用户级线程,由编程语言或库来管理,而不受操作系统的直接控制。
- 协程之间的切换通常非常高效,因为它们不需要进行完整的上下文切换。
- 协程通常用于编写高效且可伸缩的并发代码,例如在 Go 语言中的 Goroutine。
总结:
- 进程是操作系统中的独立执行单位,线程是进程内的执行单位,而协程是更轻量级的执行单位。
- 进程间通信需要IPC,线程共享进程内的内存,协程通常由编程语言或库来管理。
- 进程和线程通常由操作系统管理,协程是用户级的并发机制,更容易实现高效的并发编程。
goroutine(协程)
goroutine
是 Go 语言并行设计的核心。goruntine
其实就是协程,它比线程更小、更轻量。
执行 goroutine
只需要极光的内存(大概 4~5KB),它会根据相应的数据进行伸缩。也正因如此,程序可以同时运行成千上万个并发任务。
goruntine
是通过 Go 程序的 runtime
管理的一个线程管理器。goruntine
通过 go
关键字实现。
goroutine
是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。
当我们在编写某个并发任务时,只需要把这个任务包装成一个函数,然后开启一个 goruntine
去执行这个函数,就可以了。
go 关键字
Go 中使用 goruntine 只需要在函数或方法前加上 go
关键字就可以创建一个 goruntine
,从而让该函数或方法在新创建的 goruntine 中执行。其方式如下:
go
// 使用 go 关键字,在调用方法前加上即可
go f()
匿名函数
go func() {
//
}()
一个 goruntine 必须对应一个函数或方法,可以创建多个 goruntine 去执行相同的函数或方法。
基本案例
go 案例
package main
import "fmt"
func main() {
go say()
fmt.Println("学习 Go goruntine")
}
func say() {
fmt.Println("go goruntine")
}
匿名函数案例
package main
import "fmt"
func main() {
go func() {
fmt.Println("go goruntine")
}()
fmt.Println("学习 Go goruntine")
}
学习 Go goruntine
Process finished with the exit code 0
在这两个案例中,可能会出现 go goruntine
没有被打印出来的情况,这是为什么?
Go 程序在启动时,就会为 main
函数创建一个默认的 goruntine
。在这两个案例中,我们是在 main 函数中使用了 go
关键字创建新的 goruntine 去执行 say
函数,而此时 main goruntine 还在往下执行,此时程序中存在 2 个并发执行的 goruntine。
当 main 函数结束时, 整个程序也就结束了,同时 main goruntine 也结束了,所以由 main goruntine 创建的新的 goruntine 也会一起退出。也就是说新创建的 goruntine 还没有执行,程序就已经退出了。所以 go goruntine
也就没有打印出来了。
如何才能让新创建的 goruntine 执行?得想办法让 main goruntine
等一等。如下案例让程序等 1 秒,也就得到了我们想要的输出了。
go func() {
fmt.Println("go goruntine")
}()
fmt.Println("学习 Go goruntine")
// 等 1 秒
time.Sleep(time.Second)
go goruntine
和 学习 Go goruntine
谁先被打印?这个没有办法确定。
sync 包
上面的案例中,我们是通过time.Sleep(time.Second)
强制程序等待 1 秒,这个做法并不准确。最好的做法是得告诉主程序 goruntine 程序是否执行完毕。Go 语言提供了 sync
包来实现这一想法。
package main
import (
"fmt"
"sync"
)
// 全局等待组
var wg sync.WaitGroup
func main() {
// 标记有 1 个 goruntine 任务
wg.Add(1)
go say()
fmt.Println("学习 Go 协程")
// 阻塞等待登记的 goruntine 完成
wg.Wait()
}
func say() {
fmt.Println("Go goruntine")
// 告诉当前 goruntine 代码执行完毕
wg.Done()
}
多个 goruntine
上面的案例都是一个一个执行,怎么多个执行?
在 Go 语言中,实现并发很简单,启动多个 goruntine 即可,如下案例:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
for i := 0; i < 10000; i++ {
// 创建一个 goruntine 就登记 +1
wg.Add(1)
go Say(i)
}
wg.Wait()
}
func Say(i int) {
// goruntine 结束时 -1
defer wg.Done()
fmt.Println("学习 Go goruntine, ", i)
}
输出结果如下:
...
学习 Go goruntine, 9932
学习 Go goruntine, 9993
学习 Go goruntine, 9768
学习 Go goruntine, 9962
学习 Go goruntine, 9935
每次输出的结果是不一样的,这是因为 10000 个 goruntine 是并发执行的,而 goruntine 的调度是随机的。
GOMAXPROCS
GOMAXPROCS
是一个用于设置 Go 程序并发执行的最大 CPU 核心数的环境变量。下面是一个简单的 Go 代码示例,演示如何使用 GOMAXPROCS
来设置最大 CPU 核心数:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 获取当前系统的 CPU 核心数
numCPU := runtime.NumCPU()
fmt.Printf("Current CPU cores: %d\n", numCPU)
// 设置最大 CPU 核心数为2
runtime.GOMAXPROCS(2)
// 获取已设置的 CPU 核心数
maxProcs := runtime.GOMAXPROCS(0)
fmt.Printf("Max CPU cores: %d\n", maxProcs)
// 创建一个等待组,用于等待 Goroutine 完成
var wg sync.WaitGroup
// 启动多个 Goroutine
for i := 1; i <= 4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d is using CPU core %d\n", id, runtime.NumCPU())
}(i)
}
// 等待所有 Goroutine 完成
wg.Wait()
}
在这个示例中,我们首先使用 runtime.NumCPU()
获取当前系统的 CPU 核心数,然后使用 runtime.GOMAXPROCS()
来设置最大 CPU 核心数为2。接着,我们使用 runtime.GOMAXPROCS(0)
获取已设置的 CPU 核心数。
随后,我们创建一个等待组(sync.WaitGroup
)来等待多个 Goroutine 完成。我们启动了4个 Goroutine,每个 Goroutine 打印自己正在使用的 CPU 核心编号。
运行这个程序,你会看到输出类似以下内容:
Current CPU cores: 16
Max CPU cores: 2
Goroutine 4 is using CPU core 16
Goroutine 1 is using CPU core 16
Goroutine 2 is using CPU core 16
Goroutine 3 is using CPU core 16
这个示例演示了如何使用 GOMAXPROCS
来设置最大 CPU 核心数,以及如何在多核 CPU 上并行执行 Goroutine。在这种情况下,我们将最大 CPU 核心数设置为2,因此 Goroutine 在两个核心上并行执行。