【Go】 协程和 channel
本文详解Go协程、channel、select及线程安全。先以WaitGroup解决主线程不等协程,展示无缓冲与缓冲channel数据传递,用select多路复用与超时控制。最后剖析并发读写变量与map的竞态,给出Mutex锁与sync.Map两种同步方案,并附完整代码示例。
一:🔥 协程
Goroutine 是 Go 运行时管理的轻量级线程
在 go 中,开启一个协程是非常简单的
package main
import (
"fmt"
"time"
)
func sing() {
fmt.Println("唱歌")
time.Sleep(1 * time.Second)
fmt.Println("唱歌结束")
}
func main() {
go sing()
go sing()
go sing()
go sing()
time.Sleep(2 * time.Second)
}
如果我把这个主线程中的延时去掉之后,你会发现程序没有任何输出就结束了
这是为什么呢
那是因为主线程结束协程自动结束,主线程不会等待协程的结束
🦋 WaitGroup
我们只需要让主线程等待协程就可以了,它的用法是这样的
package main
import (
"fmt"
"sync"
"time"
)
var (
wait = sync.WaitGroup{}
)
func sing() {
fmt.Println("唱歌")
time.Sleep(1 * time.Second)
fmt.Println("唱歌结束")
wait.Done()
}
func main() {
wait.Add(4)
go sing()
go sing()
go sing()
go sing()
wait.Wait()
fmt.Println("主线程结束")
}
二:🔥 channel
有没有想过一个问题,我在协程里面产生了数据,咋传递给主线程呢?
或者是怎么传递给其他协程函数呢?
这个时候 channel 来了
基本定义
package main
import "fmt"
func main() {
var c chan int // 声明一个传递整形的通道
// 初始化通道
c = make(chan int, 1) // 初始化一个 有一个缓冲位的通道
c <- 1
//c <- 2 // 会报错 deadlock
fmt.Println(<-c) // 取值
//fmt.Println(<-c) // 再取也会报错 deadlock
c <- 2
n, ok := <-c
fmt.Println(n, ok)
close(c) // 关闭协程
c <- 3 // 关闭之后就不能再写或读了 send on closed channel
fmt.Println(c)
}
当然,在同步模式下,channel 没有任何意义
需要在异步模式下使用 channel,在协程函数里面写,在主线程里面接收数据
package main
import (
"fmt"
"sync"
"time"
)
var moneyChan = make(chan int) // 声明并初始化一个长度为0的信道
func pay(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChan <- money
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
// 现在的模式,就是购物接力
//shopping("张三")
//shopping("王五")
//shopping("李四")
wait.Add(3)
// 主线程结束,协程函数跟着结束
go pay("张三", 2, &wait)
go pay("王五", 3, &wait)
go pay("李四", 5, &wait)
go func() {
defer close(moneyChan)
// 在协程函数里面等待上面三个协程函数结束
wait.Wait()
}()
for {
money, ok := <-moneyChan
fmt.Println(money, ok)
if !ok {
break
}
}
//time.Sleep(2 * time.Second)
fmt.Println("购买完成", time.Since(startTime))
fmt.Println("moneyList", moneyList)
}
如果这样接收数据不太优雅,那还有更优雅的写法
for money := range moneyChan {
moneyList = append(moneyList, money)
}
如果通道被 close,for 循环会自己结束,十分优雅
🦋 select
如果一个协程函数,往多个 channel 里面写东西,在主线程里面怎么拿数据呢?
go 为我们提供了 select,用于异步的从多个 channel 里面去取数据
package main
import (
"fmt"
"sync"
"time"
)
// 信道 存 int 类型
var moneyChan1 = make(chan int) // 声明并初始化一个长度为0的信道
var nameChan1 = make(chan string)
var doneChan = make(chan struct{})
func send(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChan1 <- money // 信道赋值语句
nameChan1 <- name
wait.Done()
}
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
// 协程
go send("zhangsan", 2, &wait)
go send("lisi", 3, &wait)
go send("wangwu", 5, &wait)
// 再开一个协程函数判断是否结束
go func() {
defer close(moneyChan1)
defer close(nameChan1)
defer close(doneChan)
wait.Wait()
// 再创建一个信道用于关闭
// close(moneyChan)
}()
// 等价于下面的写法
var moneyList []int
var nameList []string
// 多个 channel 的写法
var event = func() {
for {
select {
case money := <-moneyChan1:
moneyList = append(moneyList, money)
case name := <-nameChan1:
nameList = append(nameList, name)
case <-doneChan:
return
}
}
}
event()
fmt.Println("购买完成", time.Since(startTime))
fmt.Println("moneyList", moneyList)
fmt.Println("nameList", nameList)
}
🦋 协程超时处理
package main
import (
"fmt"
"time"
)
var done = make(chan struct{})
func event() {
fmt.Println("event执行开始")
time.Sleep(2 * time.Second)
fmt.Println("event执行结束")
close(done)
}
func main() {
go event()
select {
case <-done:
fmt.Println("协程执行完毕")
case <-time.After(1 * time.Second):
fmt.Println("超时")
return
}
}
三:🔥 线程安全
什么是线程安全?
现在有两个协程,同时触发,一个协程对一个全局变量进行 100 完成 ++ 操作,另一个对全局变量—的操作
那么,两个协程结束,最后的值应该是0才对
package main
import (
"fmt"
"sync"
)
var num int
var wait sync.WaitGroup
func add() {
for i := 0; i < 1000000; i++ {
num++
}
wait.Done()
}
func reduce() {
for i := 0; i < 1000000; i++ {
num--
}
wait.Done()
}
func main() {
wait.Add(2)
go add()
go reduce()
wait.Wait()
fmt.Println(num)
}
但是你会发现,这个输出的结果完全无法预测
这是为什么呢?
根本原因是 CPU 的调度方法为抢占式执行,随机调度
🦋 同步锁
那么我们能不能通过给操作加锁来解决这个问题呢
答案是可以的
package main
import (
"fmt"
"sync"
)
var num int
var wait sync.WaitGroup
var lock sync.Mutex
func add() {
// 谁先抢到了这把锁,谁就把它锁上,一旦锁上,其他的线程就只能等着
lock.Lock()
for i := 0; i < 1000000; i++ {
num++
}
lock.Unlock()
wait.Done()
}
func reduce() {
lock.Lock()
for i := 0; i < 1000000; i++ {
num--
}
lock.Unlock()
wait.Done()
}
func main() {
wait.Add(2)
go add()
go reduce()
wait.Wait()
fmt.Println(num)
}
🦋 线程安全下的 map
如果我们在一个协程函数下,读写 map 就会引发一个错误
concurrent map read and map write
希望大家见到这个错误,就能知道,这个就是 map 的线程安全错误
package main
import (
"fmt"
"sync"
"time"
)
var wait sync.WaitGroup
var mp = map[string]string{}
func reader() {
for {
fmt.Println(mp["time"])
}
wait.Done()
}
func writer() {
for {
mp["time"] = time.Now().Format("15:04:05")
}
wait.Done()
}
func main() {
wait.Add(2)
go reader()
go writer()
wait.Wait()
}
我们不能在并发模式下读写 map
如果要这样做
- 给读写操作加锁
- 使用sync.Map
加锁
package main
import (
"fmt"
"sync"
"time"
)
var wait sync.WaitGroup
var mp = map[string]string{}
var lock sync.Mutex
func reader() {
for {
lock.Lock()
fmt.Println(mp["time"])
lock.Unlock()
}
wait.Done()
}
func writer() {
for {
lock.Lock()
mp["time"] = time.Now().Format("15:04:05")
lock.Unlock()
}
wait.Done()
}
func main() {
wait.Add(2)
go reader()
go writer()
wait.Wait()
}
sync.Map
package main
import (
"fmt"
"sync"
"time"
)
var wait sync.WaitGroup
var mp = sync.Map{}
func reader() {
for {
fmt.Println(mp.Load("time"))
}
wait.Done()
}
func writer() {
for {
mp.Store("time", time.Now().Format("15:04:05"))
}
wait.Done()
}
func main() {
wait.Add(2)
go reader()
go writer()
wait.Wait()
}
其实看它源码,它的内部也是用了同步锁的
四:🔥 共勉
😋 以上就是我对 【Go】协程和 channel 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
更多推荐


所有评论(0)