1. 协程(Goroutine)
协程是轻量级线程,KB级别,可以高效地利用内存资源,Go语言中用go关键字开启一个协程,通过通道进行安全地并发通信和数据传递。
线程是轻量级进程,MB级别,其创建和销毁需要操作系统的支持。线程的并发执行需要使用锁,信号量等同步原语。
协程代码示例
package main
import (
"fmt"
"time"
)
func main() {
// 创建10个协程并发执行任务
for i := 0; i < 10; i++ {
go worker(i)
}
// 等待协程执行完毕
time.Sleep(2 * time.Second)
fmt.Println("All workers have completed.")
}
func worker(id int) {
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d completed\n", id)
}
运行上述代码,输出的顺序不固定,主线程只会等待足够的时间,不会等待所有协程执行完
2. 通道
通道分为有缓冲通道和无缓冲通道,用 make 函数创建通道,函数中的参数用于指定通道的数据类型和缓冲区大小。
向通道发送和接收数据用<-运算符
package main
import (
"fmt"
)
func main() {
// 创建一个通道
ch := make(chan int)
// 启动一个协程发送数据到通道
go func() {
defer close(ch) /* 延迟关闭通道,defer用于确保通道被访问时还存在,放在第一 行确保协程执行结束后关闭通道*/
for i := 0; i < 5; i++ {
fmt.Println("Sending:", i)
ch <- i // 发送数据到通道
}
}()
// 从通道接收数据并打印
for value := range ch {
fmt.Println("Received:", value)
}
}
向通道发送数据时如果通道已满,则发送操作会阻塞,直到有空间可以发送;当从通道接收数据时,如果通道为空,则接收操作会阻塞,直到有数据可接收。这种同步阻塞的特性使得协程可以有效地进行同步操作。对于有缓冲区的通道,在发送数据时,如果通道未满,则发送操作立即完成;接收数据时,如果通道不空,则接收操作立即完成。
通道可以限制为单向的,即只能接收或发送数据。通过在通道类型前加上 <- 表示只能发送,加在后面表示只能接收。例如:chan<- int 表示只能发送 int 类型的通道,<-chan int 表示只能接收 int 类型的通道。
可以通过调用 close 函数来关闭通道,关闭后的通道不能再发送数据,但可以继续接收已经发送的数据。接收关闭的通道会立即得到零值。
3. Sync包下并发操作
3.1 Lock
Sync包下的锁有互斥锁Mutex和读写锁RWMutex。
3.1.1 mutex互斥锁
-
互斥锁用于保护临界区,确保只有一个协程可以进入临界区执行代码,从而避免多个协程同时访问共享资源导致的数据竞争和错误。
-
创建互斥锁:可以使用
var mutex sync.Mutex来声明和创建一个互斥锁变量。 -
加锁和解锁:在进入临界区之前,通过调用
mutex.Lock()来获取互斥锁,锁定当前协程,确保只有一个协程可以进入临界区。在临界区执行完毕后,通过调用mutex.Unlock()来释放互斥锁,允许其他协程进入临界区。 -
注意事项:
- 在调用
Lock()之后,确保一定会调用Unlock(),否则可能会导致死锁。 - 不要在临界区内进行阻塞操作,以免造成其他协程的长时间等待。
- 在调用
3.1.2 RWmutex读写锁
-
读写锁是一种更高级的锁机制,可以同时支持多个协程进行读操作,但只允许一个协程进行写操作。
-
创建读写锁:可以使用
var rwMutex sync.RWMutex来声明和创建一个读写锁变量。 -
读锁和写锁:
- 读锁(共享锁)可以被多个协程获取,只要没有协程持有写锁。通过调用
rwMutex.RLock()来获取读锁,调用rwMutex.RUnlock()来释放读锁。 - 写锁(排他锁)只允许一个协程获取,在写锁被持有时,其他协程无法获取读锁或写锁。通过调用
rwMutex.Lock()来获取写锁,调用rwMutex.Unlock()来释放写锁。
- 读锁(共享锁)可以被多个协程获取,只要没有协程持有写锁。通过调用
-
注意事项:
- 读操作频繁且不会改变共享资源时,多个协程可以同时获取读锁,提高并发性能。
- 写操作会阻塞其他协程的读和写操作,只有写锁被释放后,其他操作才能继续。
- 不要在持有读锁或写锁的情况下再次获取对应的锁,否则会导致死锁。
package main
import (
"fmt"
"sync"
)
var (
counter int
wg sync.WaitGroup
mutex sync.Mutex
rwMutex sync.RWMutex
)
func main() {
// 互斥锁示例
wg.Add(2)
go incrementWithMutex("goroutine 1")
go incrementWithMutex("goroutine 2")
wg.Wait()
fmt.Println("Counter value with mutex:", counter)
// 读写锁示例
wg.Add(5)
go readFromCounter("reader 1")
go readFromCounter("reader 2")
go readFromCounter("reader 3")
go writeToCounter("writer 1")
go writeToCounter("writer 2")
wg.Wait()
fmt.Println("Counter value with RWMutex:", counter)
}
// 使用互斥锁递增 counter
func incrementWithMutex(name string) {
defer wg.Done()//函数执行完毕后通知waitgroup
for i := 0; i < 5; i++ {
mutex.Lock() // 加锁
counter++ // 临界区递增操作
fmt.Println(name, "incremented counter to", counter)
mutex.Unlock() // 解锁
}
}
// 使用读写锁读取 counter
func readFromCounter(name string) {
defer wg.Done()
for i := 0; i < 3; i++ {
rwMutex.RLock() // 获取读锁
fmt.Println(name, "read counter value as", counter)
rwMutex.RUnlock() // 释放读锁
}
}
// 使用读写锁递增 counter
func writeToCounter(name string) {
defer wg.Done()
for i := 0; i < 2; i++ {
rwMutex.Lock() // 获取写锁
counter++ // 临界区递增操作
fmt.Println(name, "incremented counter to", counter)
rwMutex.Unlock() // 释放写锁
}
}
3.2 WaitGroup
sync.WaitGroup 是 Go 语言中用于等待一组协程完成工作的同步原语。它提供了线程等待功能,可以在协程之间同步执行,并且可以确保在协程完成工作之前不会继续执行主协程或其他协程。
WaitGroup 提供了以下三个方法:
Add(delta int):用于添加需要等待的协程数量,将WaitGroup的计数器加上delta。通常在创建一个协程之前调用Add(1),表示要等待一个协程的完成。Done():用于通知WaitGroup协程的工作已经完成,将计数器减一。通常在协程的最后调用Done()来标记协程的工作完成。Wait():用于阻塞主协程,直到WaitGroup的计数器归零。Wait()方法会一直阻塞,直到计数器变为零,表示所有协程的工作都已完成。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 添加一个协程到 WaitGroup 计数器
wg.Add(1)
go func() {
defer wg.Done()
// 协程的工作逻辑
fmt.Println("Hello from goroutine")
}()
fmt.Println("Waiting for goroutine to finish...")
wg.Wait()//阻塞,直到计数器归零,即所有协程执行完毕
fmt.Println("All goroutines have finished")
}