go协程特点
一个线程上可以起多个goroutine,goroutine有以下特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
下面是一个简单的协程使用案例:
package main
import(
"fmt"
"strconv"
)
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test func:" + strconv.itoa(i))
time.Sleep(time.second)
}
}
func main() {
// 开启一个协程
go test()
for i := 0; i <= 10; i++ {
fmt.Println("main func:" + strconv.itoa(i)
time.Sleep(time.second)
}
}
如果主线程退出了,协程即时还没执行完也会退出,因此我们通常还需要加一些同步机制;
协程的MPG调度模型
M是线程,P是协程的上下文,G是协程
1、假设当前M0正在执行G0协程,如果G0协程阻塞读文件或网络数据时,这时系统会取出M1线程执行;
2、取出M1线程后,系统会将执行M1挂的三个协程执行,这时M0仍在执行阻塞操作中,等G0不再阻塞之后,M0又会被放到空闲主线程继续执行,G0会被唤醒
golang设置运行cpu数量
我们可以通过runtime包的函数来设置cpu数,go1.8之后默认让程序运行在多个cpu上,1.8之前需要人工设置
func main() {
// 获取cpu数量
cpu_num := runtime.RuntimeCPU()
// 设置使用的cpu数量
runtime.GOMAXPROCS(cpu_num - 1)
}
goroutine资源竞争及解决方案
多个goroutine之间操作同一个数据时,就会出现资源竞争问题,go语言在编译程序时也提供了一个-race参数来帮助开发者判断是否存在资源竞争。有两种方法来解决资源竞争:
(1)对竞争资源加锁;使用sync包里面的同步函数,不推荐使用
// 定义一个互斥锁变量
lock sync.Mutex
lock.Lock()
lock.Unlock()
(2)使用管道(推荐);
管道(channel)的基本介绍
管道本身就是一个先入先出的队列,但它是一个线程安全的队列,同时有类型,string类型的channel只能存储string类型的数据,虽然我们也可以用interface来存储多类型,但用的时候也需要用类型断言判断类型后再使用,声明管道的方式如下:
var 管道名 chan 数据类型
var Intchan chan int
var Mapchan chan map[string]int
var Personchan chan Person
管道是引用类型,使用前必须用make开辟内存初始化,管道的使用简单案例如下:
func main() {
// 管道创建与初始化
var IntChan chan int
IntChan = make(chan int, 3)
// 管道写入数据
IntChan<- 5
num := 100
IntChan<- num
// 从管道中读取数据
out_num := <-IntChan
}
需要注意的是,channel中的数据如果已经取完了,再取会报错,报死锁,如果channel满了,就不能再放入了,再放也会报错
空接口作为管道数据类型
如果想让管道中存放多种类型的数据,需要将管道的数据类型声明为空接口下面是一个案例
type Cat struct {
Name : string
Age : int
}
func main() {
// 定义一个空接口数据类型的管道
var allchan chan interface{}
allcha := make(char interface{}, 3)
allchan<- 10
allchan<- "Tom"
cat := Cat{"xiaomao", 1}
allchan<- cat
// 推出前两个数据
<-allchan
<-allchan
mycat := <-allchan
// 这里打印出来cat类型和值是没问题的
fmt.Printf("mycat type:%T, val:%v", mycat, mycat)
// 在不使用类型断言之前,是不能直接把mycat当Cat类型使用的
fmt.Printf("my cat name:%v", mycat.Name) // 会报错
a := mycat.(Cat)
fmt.Printf("mycat name:%v", a.Name)
}
channel的关闭和遍历
- 使用内置函数close可以关闭channel,channel关闭后,可以读,但不能再继续往里面写
- 管道只能使用for range的方式遍历,因为len会在减少
- 在遍历时如果没有close管道,会出现deadlock(一直在等待数据写入)
close(IntChan)
for v := IntChan {
fmt.Println("v:%v", v)
}
管道和goroutine的综合案例
package main
import(
"fmt"
)
func writeData(intchan chan int) {
for i := 1; i < 50; i++ {
intchan<- i
}
close(intchan)
}
func ReadData(intchan chan int, exitchan chan bool) {
for {
v, ok := <-intchan
if !ok {
break
}
fmt.Printf()
}
exitchan<- true
close(exitchan)
}
func main() {
var intchan chan int = make(chan int, 50)
exitchan := make(chan bool, 1)
go writeData(intchan)
go ReadData(intchan, exitchan)
for v := exitchan {
break
}
}
需要注意的是,如果我们值往管道里面读,而没有往管道里面写的操作,那么就会报死锁错误,只要有写的操作,那么即时读写频率不一致,也不会死锁。
管道的使用细节
- 默认情况下,管道是双向管道,我们可人工指定管道为只读或只写
func main() {
var writeOnlyChan chan<- int
var readOnlyChan <-chan int
writeOnlyChan = make(chan int, 3)
readOnlyChan = make(chan int, 3)
}
// 除了上面的场景外,多数情况我们是在函数参数里面声明管道为只读或只写
func recv(ch <-chan int) {
// ...
}
func send(ch chan<- int) {
// ...
}
// 这时我们可以将一个双向管道作为参数传入,但在函数内部,双向管道将被限定只读或只写
var IntChan chan int
IntChan = make(chan int, 5)
recv(Intchan)
send(IntChan)
- 使用select可以解决从管道读数据阻塞的问题
实际开发中不好确定什么时候关闭管道,在遍历管道时,如果不关闭管道,容易导致死锁(写操作没关,但没继续写了,读操作阻塞),我们可以用select来解决这个问题。
func main() {
intChan := make(chan int, 3)
strChan := make(chan string, 5)
for i := 0; i < 3; i++ {
intChan<- i
}
for j := 0; j < 5; j++ {
strChan<- "str" + fmt.Sfrintf("%d", i)
}
for {
select {
case v := <-intChan :
fmt.Printf("read from intchan;%v", v)
case v := <-strChan :
fmt.Printf("read from strChan:%v", v)
default:
return
}
}
}
- 协程中的panic捕获,避免程序崩溃
我们可以使用recover来捕获协程中的panic,避免影响其他协程
func test() {
// 匿名函数捕获异常
defer func() {
if err := recover(); err != nil {
fmt.Println("test panic", err)
}
}()
var tmpMap map[int]string
map[0] = "make panic"
}
func main() {
go test()
}