并发编程就是可以让你的程序不是顺序执行的,而是可以多个分支同时进行,在go中,这个分支也被称为协程goroutine(轻量级线程)。
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。main函数结束,所有的goroutine都会结束。
package main
import (
"fmt"
"sync"
)
var total struct{
sync.Mutex
value int
}
func worker(wg *sync.WaitGroup) {
defer wg.Done() //计数器值减一
for i := 0; i < 100; i++ {
total.Lock() //加锁,其他线程想加锁会发生阻塞,保证加锁范围内的语句同一时刻只会有一个线程访问
total.value += i
total.Unlock() //解锁
}
}
func main() {
var wg sync.WaitGroup //计数器,初始值0
wg.Add(2) //对计数器进行加2
wg.Add(1)
go worker(&wg) //开启go routine
go worker(&wg) //计数器要传引用
go worker(&wg)
wg.Wait() //阻塞,知道计数器值变为0
fmt.Println(total.value)
}
上例中可以看到有加锁语句,对多线程模型的程序而言,原子性操作进行加锁和解锁都是有必要的。所谓的原子性操作,就是并发编程中“最小的且不可并行化的”操作。假如上文中的total.value += i同时有两个线程执行这句,假如此时value=2,i都为5,则执行这两句后,得到的结果为7,而不是12,所以可能会导致结果不正确。我们也可用sync/atomic包来进行原子性操作(推荐):
func worker(wg *sync.WaitGroup) {
defer wg.Done() //计数器值减一
for i := 0; i < 100; i++ {
atomic.AddInt64(&total.value, int64(i))
}
}
顺序一致性内存模型
顺序一致性内存模型有两大特性。
- 一个线程中所有操作必须按照程序的顺序来执行。
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
在Go语言中,同一个Goroutine线程内部,顺序一致性的模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性的内存模型。需要通过明确定义的同步事件来作为同步的参考。
goroutine奉行通过通信来共享内存,其中,通道是在goroutine之间进行同步的主要方法。用法如下:
package main
import "fmt"
var chan1 = make(chan bool) //建立一个无缓存通道(通道的缓存大小是0)
var chan2 = make(chan bool)
func work(job string) {
fmt.Println("I want to work: ", job)
chan1 <- true // 无缓存通道的发送操作总在对应的接收操作前发生
}
func study(book string) {
fmt.Println("I want to study: ", book)
close(chan1) //关闭通道后,接收者可以从通道中接收到零值
}
func play(game string) {
fmt.Println("I want to play: ", game)
<- chan2 // 无缓存通道的接收总在对应该通道的发生完成前
}
func main() {
go work("程序员")
go study("go语言")
go play("王者荣耀")
<- chan1
<- chan1
chan2 <- false
}
对于带缓存的通道,对于通道中的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是通道的缓存大小。我们可以通过控制通道的缓存大小来控制并发执行的goroutine的最大数目。
例如:(此处仅当示例)
package main
import "fmt"
var chan1 = make(chan bool) //建立一个无缓存通道(通道的缓存大小是0)
var chan2 = make(chan bool)
var chan3 = make(chan bool, 3)
func work(job string) {
fmt.Println("I want to work: ", job)
//chan1 <- true // 无缓存通道的发送操作总在对应的接收操作前发生
chan3 <- true
}
func study(book string) {
fmt.Println("I want to study: ", book)
//close(chan1) //关闭通道后,接收者可以从通道中接收到零值
chan3 <- true
}
func play(game string) {
fmt.Println("I want to play: ", game)
//<- chan2 // 无缓存通道的接收总在对应该通道的发生完成前
chan3 <- true
}
func main() {
go work("程序员")
go study("go语言")
go play("王者荣耀")
<- chan3
<- chan3
<- chan3
//<- chan1
//chan2 <- false
}