关于go协程同步的几种方法
这里咋们简要介绍下关于go中协程的几种同步方法。先说下概念性的:
协程概念简要理解
协程有点类似线程,是一种更为轻量级的调度单位,但协程还是不同于线程的,线程是系统级实现的,常见的调度方法是时间片轮转法,比如每隔10s切换一个线程执行。
协程则是应用软件级实现,它和线程的原理差不多,当一个协程调度到另一个协程时,将上一个协程的上下文信息压入堆栈,来回切换。一个线程可以跑很多个协程,由这个线程来调度协程的切换,如果是C/C++的话底层就能通过select/poll/epoll来做,比如微信后台的开源libco库。
go协程底层不是C,纯go实现的,go的协程应该是目前各类有协程概念的语言中实现的最完整和成熟的,调度是基于GPM模型实现的,有兴趣可以去了解下,这里我也不扯远了,下面看看协程的同步。
为什么要做同步
至于为什么需要同步呢,类似线程要做同步差不多,现在的cpu都是多核,如果一核一个线程同时一起访问同一块内存中的数据吗?那么可能上一个线程(第一个线程)刚把数据从寄存器拷贝到内存,第二个线程马上又把此数据用它修改的值给覆盖了,这样共享数据变会乱套。
举个例子 :
用3个协程序并发各自增一个全局变量10000次
package main import ( "fmt" "time" ) var share_num uint64 = 0 func incrNum() { for i := 0; i < 10000; i++ { share_num++ } fmt.Println(share_num) } func main() { for i := 0; i < 3; i++ { go incrNum() } time.Sleep(5 * time.Second) }
输出:
10000 13351 23351
运行多次 , 可以看到我们虽然自增了10000次,但没有一个输出30000(3个协程自增)的结果。那么本文重点来了:
协程的常见同步方法
Mutex
互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。
package main import ( "fmt" "sync" "time" ) var share_num uint64 = 0 var lck sync.Mutex func incrNum() { for i := 0; i < 10000; i++ { lck.Lock() share_num++ lck.Unlock() } fmt.Println(share_num) } func main() { for i := 0; i < 3; i++ { go incrNum() } time.Sleep(5 * time.Second) }
输出:
22073 27091 30000
可以看到其中一个协程最后输出了30000,下面我们通过管道channel实现。
channel
使用go的channel, 下面一个典型的生产消费模型:
package main import ( "fmt" "strconv" "time" ) func main() { msg_chan := make(chan string) done := make(chan bool) i := 0 go func() { for { i++ time.Sleep(1 * time.Second) msg_chan <- "消息发送:在吗?" <-done // 这里从管道done 取值,必须要有发送的地方,不然阻塞 } }() go func() { for { select { case msg := <-msg_chan: i++ fmt.Println(msg + " 当前循环i:" + strconv.Itoa(i)) time.Sleep(2 * time.Second) done <- true // 发送 true 到done } } }() time.Sleep(20 * time.Second) }
输出:
消息发送:在吗? 当前循环i:2 消息发送:在吗? 当前循环i:4 消息发送:在吗? 当前循环i:6 消息发送:在吗? 当前循环i:8 消息发送:在吗? 当前循环i:10 消息发送:在吗? 当前循环i:12 消息发送:在吗? 当前循环i:14
WaitGroup
sync包中的WaitGroup可用等待一组协程的结束,父协程通过Add方法来设定应等待的线程的数量,每个被等待的协程在结束时调用Done方法,
同时,主协程里调用Wait方法阻塞直至所有线程结束。
package main import ( "fmt" "net/http" "sync" ) var wg sync.WaitGroup var urls = []string{ "https://www.mi.com/", "http://www.baidu.com/", "https://www.jd.com/", } func main() { for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() fmt.Print("结束") }
以上是关于go协程同步的常用处理方式。关于更多的协程同步技巧,欢迎诸位拍砖留言。