并发编程就是可以让你的程序不是顺序执行的,而是可以多个分支同时进行,在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,所以可能会导致结果不正确。上面的是加的是互斥锁,go中还有读写锁sync.RWMutex
我们也可用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
}
对于带缓存的通道,其中C是通道的缓存大小,则表示通道最多可储存C个值,达到这个值还没有被接收,则发送会被阻塞。我们可以通过控制通道的缓存大小来控制并发执行的goroutine的最大数目。可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。
例如:(此处仅当示例)
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
}
通过select和default分支可以很容易实现一个goroutine的退出控制:
package main
import (
"fmt"
"time"
)
func work2(channel chan bool, phone chan int) {
for {
time.Sleep(time.Second)
select {
default:
fmt.Println("I am 工作")
case <- channel:
fmt.Println("I am 下班")
phone <- 1
}
}
}
func main() {
ch := make(chan bool)
phone := make(chan int, 10)
for i := 0; i < 10; i++ {
go work2(ch, phone)
}
time.Sleep(time.Second * 3)
close(ch)
for i := 0; i < 10; i++ {
<-phone
}
}
上例也可用sync.WaitGroup,结合defer来进行改进。
package main
import (
"fmt"
"sync"
"time"
)
func work2(channel chan bool, wg *sync.WaitGroup) {
defer wg.Done()
for {
time.Sleep(time.Second)
select {
default:
fmt.Println("I am 工作")
case <- channel:
fmt.Println("I am 下班")
return
}
}
}
func main() {
ch := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go work2(ch, &wg)
}
time.Sleep(time.Second * 3)
close(ch)
wg.Wait()
}
关闭通道close
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。
如果你的通道不往里存值了记得关闭通道
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
fmt.Println("协程", i)
}
close(c) //假如不关闭通道,下面的线程还是会认为这个通道能够取出值,会陷入deadlock
}()
for {
if data, ok := <-c; ok { // 用ok判断channel是否能取出值
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}
context包 上下文
使用例子:
//素数筛,并用context包来避免后台goroutine内存泄漏
package main
import (
"context"
"fmt"
)
//生成自然数序列
func GeneralNatural(ctx context.Context) chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
select {
case <-ctx.Done():
return
default:
ch <- i
}
}
}()
return ch
}
//通道过滤器: 删除能被素数整除的数
func PrimerFilter(ctx context.Context, in <- chan int, primer int) chan int{
out := make(chan int)
go func() {
for {
var i int
if i = <-in; i % primer != 0 {
select {
case <-ctx.Done():
return
default:
out <- i //新的序列,比原来少了能被primer整除的数
}
}
}
}()
return out
}
func main() {
// 通过Context控制后台Goroutine状态
ctx, cancel := context.WithCancel(context.Background())//返回其子context和取消函数cancel
ch := GeneralNatural(ctx) // 生产自然数序列 2,3,4,5,.....
for i := 0; i < 100; i++ {
prime := <-ch //新出现的素数
fmt.Printf("%v: %v\n", i+1, prime)
ch = PrimerFilter(ctx, ch, prime) //过滤掉能被新出现的素数整除的数
}
cancel()// cancel调用,即会往子Context的Done通道发送消息
}