简介
Go语言天生支持并发编程,通过goroutine开启协程并发,通过channel实现协程通信,然后通过sync等库保证并发的性能和安全。所以这里我们关注三个问题:
- 怎么使用goroutine?协程是怎么执行的?
- 协程通信有哪些方式,怎么通过channel实现协程通信?
- 如何保证协程安全?
Goroutine
Goroutine本身是一个函数,直接调用就是一个函数,通过go关键字调用就是一个goroutine协程。main函数对应的是main goroutine,Go程序启动入口在runtime.main,会创建main goroutine,执行初始化逻辑,然后在main goroutine中执行我们编写的main函数,遇到go关键字时,再创建新的goroutine。main goroutine执行完我们的main函数之后,会执行runtime剩余的Defer和Panic处理逻辑,或者直接exit。
func helloWorld(){
fmt.Print("hello,world")
}
//goroutine调用方式
go helloWorld(){}
也可以直接调用一个匿名函数:
go func(){
fmt.Print("hello,world")
}()
### Goroutine的优雅退出
在main goroutine中,如果执行完了我们编写的main.main,还有其它的goroutine没有执行,而且没有使用WaitGroup、channel同步机制的话,runtime会带着整个程序一起退出,不会管那些没有执行完毕的goroutine。例如,下面这段代码不会有任何输出,因为main在创建完协程之后马上退出了。
```golang
package main
import "fmt"
func helloWorld() {
fmt.Print("hello,world")
}
func main() {
go helloWorld()
}
在go helloWorld()后面加上time.Sleep(600),可以看到有输出。但是这种方式算不上优雅,且不能保证在这个时间之内,所有的goroutine都执行完了。这里先总结一些优雅退出的方法,可以先看后面的部分,再回头看这里的优雅退出方法。我们可以通过信道,从其它的goroutine向主协程发送完成消息,告诉主协程可以,但是这种方式不适合很多goroutine的情况。
package main
import "fmt"
func main() {
done := make(chan bool)
go func() {
fmt.Println("Hello World")
done <- true
}()
<-done
}
使用sync.WaitGroup优雅退出 这是一个只要实例化了就可以调用的类型,有三个方法:
- Add():添加本协程要等待的协程数目。通常在调用协程前使用。
- Done():子协程完成之后,调用done(),对应的计数器减一。
- Wait():阻塞当前协程,只到实例计数器归零。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("Hello World")
wg.Done()
}()
wg.Wait()
}
Channel
一个让一个goroutine和另外的goroutine传输信息的通道,又叫做信道/通道。它是线程/协程安全的。
channel创建方式
信道必须通过make初始化才能够使用,否则是一个nil。
var 实例 chan 信道类型
var chan1 chan int
pipeline = make(chan int)
pipeline:=make(chan int)
channel是可以有容量的,make函数的第二个参数可以指定channel的容量,默认为0。后面的缓冲/无缓冲信道会讲到这个容量的作用。
pipeline:=make(chan int,2)
channel的操作
channel有三种操作:发送数据、读取数据、关闭。
pipeline<-200//发送数据
data:=<-pipeline//读取数据
close(pipeline)//关闭channel
接收方从已经关闭的channel中读出的数据永远是0,发送方二次关闭channel也是会报错的,所以要学会怎么判断信道是否关闭。从信道读取数据,实际会有两个返回值,第二个表示channel是否还在正常工作,没有被关闭。
x,ok:=<-pipeline
无缓冲信道和缓冲信道
无缓冲信道是容量为0的信道,缓冲信道的容量不为0,可以分别通过cap()和len()获取信道的容量和长度。
c:=cap(pipeline)
l:=len(pipeline)
这里要注意两点,发送端和接收端都是可能出现阻塞的。
- 如果无法从信道中获取新的数据,发送端会阻塞,只到获取到数据,无论信道容量是多少。
- 如果信道已满,发送端也会阻塞。对于无缓冲信道,如果没有接收端在等着接收数据,发送端会阻塞。对于缓冲信道,如果长度等于容量,发送端同样会阻塞。
如果发送端和接收端在不同的协程,阻塞一般没什么影响,等另一端准备好了就可以。但是如果二者在同一个协程,一端阻塞,另外一端可能也没办法解决,可能会导致死锁。例如下面这个发送端阻塞了主线程,接收端等发送方完成才能创建并接受数据,而发送端在等待一个接收端,整个main进入了死锁的状态。
package main
import (
"fmt"
"time"
)
func main() {
pipeline := make(chan int, 0)
pipeline <- 200
fmt.Printf("ok")
close(pipeline)
go func() {
data, ok := <-pipeline
fmt.Println(data, ok)
}()
time.Sleep(2 * time.Second)
}
双向信道和单向信道
一般定义的chan都是双向信道。但是有些时候,我们希望对信道的数据流做一些控制,例如希望某个信道只读或者只写。golang的单向信道需要从双向信道通过别名类型来定义,因为只有只读或者只写没有意义。
package main
import (
"fmt"
"time"
)
type Sender chan<- int
type Receiver <-chan int
func main() {
pipeline := make(chan int, 0)
go func() {
var sender Sender = pipeline
sender <- 1
}()
go func() {
var receiver Receiver = pipeline
num := <-receiver
fmt.Println(num)
}()
time.Sleep(time.Second)
}
select语法
select专门用于处理多个channel的操作,类似于switch语句,但是专注于channel通信。
select {
case <-chan1:
// 从chan1接收到数据时执行
case chan2 <- value:
// 成功向chan2发送数据时执行
case val := <-chan3:
// 从chan3接收数据并赋值给val时执行
default:
// 当上述case都阻塞时执行
}
多个case准备就绪时,会选择一个随机执行,如果所有case都阻塞,而且没有default分支,select会被阻塞。
sync提供的并发支持
sync包提供的是同步原语,协调goroutine的并发关系和对共享资源的访问,避免数据竞争和死锁问题,比较常见功能包括互斥锁、读写互斥锁、等待组、只执行一次、并发安全的map。
Mutex
实现协程之间,对于共享资源的互斥访问。实例化即可访问。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
x := 9
go func() {
mu.Lock()
x = 7
mu.Unlock()
}()
fmt.Println(x)
time.Sleep(time.Second)
}
RWMutex
允许多个读操作并发,但是写操作互斥。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.RWMutex
x := 9
go func() {
mu.RLock()
fmt.Println(x)
mu.RUnlock()
}()
go func() {
mu.Lock()
x = 6
mu.Unlock()
}()
time.Sleep(time.Second)
}
WaitGroup
用于优雅等待子协程退出。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("Hello World")
wg.Done()
}()
wg.Wait()
}
Once
多个协程可能需要执行同一段代码,Once可以保证只执行一次。例如我们使用多个协程来创建单例模式。
package main
import (
"fmt"
"sync"
"time"
)
type Singleton struct {
val int
}
var (
instance *Singleton // 单例实例
one sync.Once // 确保单例实例只被创建一次
)
// GetInstance 返回单例实例
func GetInstance(v int) *Singleton {
if instance == nil { // 如果实例不存在,则创建
time.Sleep(1 * time.Millisecond) // 模拟耗时操作
fmt.Println("GetInstance v:", v)
one.Do(func() {
instance = &Singleton{val: v}
}) //只会执行一次
}
return instance
}
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
singleton := GetInstance(i)
fmt.Printf("Singleton instance: %p, value: %d\n", singleton, singleton.val)
}(i)
}
time.Sleep(1000 * time.Second)
Map
线程安全的Map。标准的map加互斥锁的方案存在严重性能问题,会粗粒度锁定整个map。sync.Map中使用了两个mao,一个专门用于读,一个专门用于写。sync.Map同样不需要初始化,sync.Map内部使用interface{}存储所有键值,值没有类型要求,但是键是需要能够比较的类型,有点类似Redis的意思。
var m sync.Map
m.Store("name","hong")//写
m.Store("age",24)
name,ok:=m.Load("name")//读
m.Delete("name")//删
还有些高级操作
val,loaded:=m.LoadOrStore("age",25)//不存在则存储,返回存储后的值
m.Range(func(key,value interface())bool{
fmt.Printf(" %v: %v\n", key, value)
return true
})
它的键值都是interface{},所以我们需要类型断言来获取正确类型的值。
value, ok := m.Load("name")
if !ok {
// 键不存在
return
}
// 类型断言
if name, ok := value.(string); ok {
fmt.Println("名字是:", name)
} else {
fmt.Println("值类型不是字符串")
}
总结
重点是三部分:
- goroutine:Golang并发基础。
- channel:协程通信基础,Golang一等公民。
- sync:高效管理并发,避免数据竞争和死锁问题。