并发
Gorountines
在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。
线程和协程
协程和线程的区别
协程(Coroutine)和线程(Thread)都是并发编程中用于实现并发执行的概念,但它们在实现方式和用途上有一些重要的区别。
-
调度方式:
- 线程是由操作系统内核进行调度和管理的,称为内核线程。线程切换由操作系统完成,切换开销较大。
- 协程是由编程语言的运行时环境(如Go语言的Go调度器)进行调度和管理的,称为用户态线程。协程切换由编程语言的运行时环境完成,切换开销较小。
-
并发性能:
- 由于线程切换是由操作系统完成的,线程的创建、销毁和切换开销较大,因此创建大量线程可能导致系统负担过重。
- 协程切换由编程语言运行时环境负责,切换开销较小,可以轻松创建大量的协程,提高并发性能。
-
内存占用:
- 线程在创建时需要分配一定的堆栈空间,同时需要维护内核数据结构,因此线程的内存占用较大。
- 协程的内存占用较小,因为它们共享相同的堆栈,无需维护内核数据结构。
-
多核利用:
- 线程可以在多核处理器上并行执行,利用多核优势。
- 一般情况下,单个协程只能在一个线程中运行,无法直接利用多核处理器,但在某些语言中,可以通过多线程和多协程的结合来实现多核利用。
-
同步方式:
- 线程之间的同步通常通过共享内存和锁等机制实现,容易出现死锁和竞态条件。
- 协程之间的同步通常通过通道(Channel)等机制实现,由于通道本身就是并发安全的,因此更容易编写线程安全的代码。
channel
make(chan 元素类型,[缓冲大小])
无缓冲通道 make(chan int)
有缓冲通道 make(chan int ,2)
案例:
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 用途包括:
- 关闭资源:例如文件、数据库连接或网络连接等,在函数结束前及时关闭它们。
- 解锁资源:在函数结束时释放被锁定的资源,以防止死锁。
- 记录日志:在函数退出前记录函数的状态、执行信息或错误日志。
- 清理资源:在函数结束前释放动态分配的内存或资源。
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执行完毕的功能,特别是在处理并发任务时。