go并发编程
预备知识
进程和线程
并发和并行
golang设置CPU
协程 - goroutine
- 一个Go主线程(也可以理解为进程)上可以起很多的协程
- 协程是轻量级线程,底层根据线程特点进行了大量优化[编译器底层优化]
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
MPG模式
管道 - channel
基本使用
//声明管道
var intChan chan int //intChan用于存放int数据
//创建一个可以存放3个int类型的管道
intChan = make(chan int, 3)
//向管道写入数据
num := 211
intChan<- 100
intChan<- num
//从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println(num2)
fmt.Printf("channel len=%v cap=%v", len(intChan), cap(intChan))
channel的遍历和关闭
//管道的遍历 - for range
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++{
intChan2<- i
}
close(intChan2) //关闭管道
//没close 死锁错误
for v := range intChan2{
fmt.Println("v = ", v)
}
并发安全问题
并发写问题
多个协程同时修改同一全局变量
//全局变量
var (
myMap = make(map[int]int, 10)
)
// 使用goroutine计算 1-200 各个数的阶乘,并把各个数的阶乘放到 map 中
func Demo3() {
//启用200个协程
for i := 1; i <= 200; i++{
go channelTest(i)
}
time.Sleep(time.Second * 10)
for key, value := range myMap {
fmt.Printf("%v! = %v", key, value)
}
}
// 计算n的阶乘,将结果放入到map中
func channelTest(n int){
res := 1
for i := 1; i<=n ; i++{
res *= i
}
myMap[n] = res
}
使用 go build -race 编译可检查是否有数据冲突(data race)
使用互斥锁(初级)
//全局变量 lock sync.Mutex
//使用互斥锁
lock.Lock()
myMap[n] = res
lock.Unlock()
var (
//全局变量
myMap = make(map[int]int, 10)
//全局变量 互斥锁
lock sync.Mutex
)
// 使用goroutine计算 1-200 各个数的阶乘,并把各个数的阶乘放到 map 中
func Demo3() {
//启用200个协程
for i := 1; i <= 200; i++{
go channelTest(i)
}
time.Sleep(time.Second )
for key, value := range myMap {
fmt.Printf("%v! = %v\n", key, value)
}
}
// 计算n的阶乘,将结果放入到map中
func channelTest(n int){
res := 1
for i := 1; i<=n ; i++{
res *= i
}
//使用互斥锁
lock.Lock()
myMap[n] = res
lock.Unlock()
}
协程结合管道
package goroutineDemo
import (
"fmt"
)
// 一个管道,两个协程
// 一个读,一个写
// 主线程需要等待协程操作完成再关闭
func Channel1() {
// 创建两个管道
intChan := make(chan int, 50) //数据管道
exitChan := make(chan bool, 1) //退出管道
go writeData(intChan)
go readData(intChan, exitChan)
//time.Sleep(time.Second * 10)
//等待协程
for{
_ , ok := <-exitChan
if !ok{
break
}
}
}
func writeData(intChan chan int){
for i:=1; i<=50; i++{
intChan<- i
fmt.Println("写入数据 ",i)
}
close(intChan)
}
func readData(intChan chan int,exitChan chan bool){
for {
v,ok := <-intChan
if !ok {
break
}
fmt.Printf("readData %v\n",v)
}
//告知主线程
exitChan<- true
close(exitChan)
}
注意事项
- channel中只能存放指定的数据类型
- channel的数据放满后再放,会报死锁错误
- 单线程 - channel没有close的情况下:channel中的数据为空再取,会报死锁错误
- 单线程 - channel已经close的情况下:channel中的数据为空再取,v,ok = <-channel 中 ok会被置为false
- 多线程 - 多线程情况下,同一管道如果只读不写或者只写不读,编译器会报错 deadlock!
- 多线程 - 多线程情况下,同一管道如果读写速率不一致,则速率快的会有效阻塞
- 只读管道 var intChan <-chan int ; 只写管道 :var intChan chan<- int
只读/只写管道可以在函数传参时限制函数内的操作方式
使用select可以解决从管道取数据的阻塞问题
//使用select解决从管道取数据的阻塞问题
//容量为10的int管道
intChan := make(chan int,10)
for i := 0; i < 10; i++ {
intChan<- i
}
//容量为5的string管道
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan<- "hello "+fmt.Sprintf("%d",i)
}
//传统方法遍历时,如果没有close管道,会阻塞导致死锁
//for {
// v := <-intChan
// fmt.Println(v)
//}
for {
select {
//此种方式,即使管道一直不关闭,也不会一直阻塞导致deadlock
//会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan中读取数据%d\n",v)
case v := <-stringChan :
fmt.Printf("从stringChan中读取数据%s\n",v)
default:
fmt.Println("都取不到了,不玩了!!!")
return
}
}
goroutine中使用defer+recover,可以解决单个协程出错影响其他协程以及主线程的问题 ```go // 一个协程报错导致程序崩溃 func RecoverDemo() { go sayHello() go errorFunc()
time.Sleep(time.Second * 5) }
func sayHello(){ for i := 0; i < 10; i++{ time.Sleep(time.Second) fmt.Println(“hello golang”) } }
func errorFunc(){ var myMap map[int]string //没有make,直接赋值 myMap[0] = “golang” }
```go
// defer + recover
func errorFunc(){
defer func() {
//捕获errorFunc抛出的panic
if err := recover(); err!=nil{
fmt.Println("函数错误!!",err)
}
}()
var myMap map[int]string
//没有make,直接赋值
myMap[0] = "golang"
}