一、groutine
为了实现并发,产生groutine
带来的问题:各种并发带来的访问竞争
解决办法:各种并发原语/同步方式
1、加锁,即通过共享内存来通信
互斥锁 sync.Mutex
读写锁sync.RWMutex
2、waitgroup:可以控制一组groutine 同时运行并等待结果返回之后再进行后续操作
add 增加计数器
done 减少计数器
wait 判断计数器值进行阻塞
踩坑1:
wg传递 为指针类型
wait阻塞的是 该wait所在的groutine,故main groutine不会被阻塞,会继续执行
package main
import (
"fmt"
"sync"
)
func writeCh(wg *sync.WaitGroup, i int, ch chan []int) {
defer wg.Done()
data := make([]int, 0)
data = append(data, i)
ch <- data
fmt.Printf("write data to ch = %v\n", data)
}
func readCh(ch chan []int) {
for data := range ch {
fmt.Printf("read ch data = %v\n", data)
}
}
func main() {
//注意:为指针类型
wg := &sync.WaitGroup{}
ch := make(chan []int)
for i := 0; i < 10; i++ {
wg.Add(1)
go writeCh(wg, i, ch)
}
go func() {
//wait阻塞的是 该wait所在的groutine,故main groutine不会被阻塞,会继续执行
wg.Wait()
fmt.Println("wait end")
close(ch)
}()
fmt.Println("start------------")
//正确方法:此时for range由于ch未被关闭而阻塞,main函数不会继续执行下去,直到上一个wait 所在的groutine将ch关闭,main才执行
for data := range ch {
fmt.Printf("read ch data = %v\n", data)
}
//错误方法:由于main groutine不会被阻塞,所以 readCh 与 main并行执行,在readCh没执行结束时,main groutine就执行结束了,故readCh读不到任何东西
//此时只会打印:
//start------------
//end
//go readCh(ch)
fmt.Println("end")
}
踩坑2:
goroutine 闭包不传递参数产生的错误
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
c := make(chan int)
//错误:
//闭包= 函数+引用的变量
//由于该变量被引用,分配在堆上,i还没写进管道,for循环就进到下一个i了
//for i := 0; i < 10; i++ {
// wg.Add(1)
// go func() {
// defer wg.Done()
// c <- i
// }()
//}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
c <- i
}(i)
}
go func() {
for msg := range c {
fmt.Println(msg)
}
}()
wg.Wait()
fmt.Println("end")
}
3、
3.1 once:保证函数只执行一次
// Go语言的入口 main() 函数所在的包(package)叫 main
// 通过 package main 该语句声明自己所在的包
// 在go中,任何一个package 都可有唯一一个带有main方法的go文件,即main.go只能有一个
package main
import (
"fmt"
"sync"
)
// 为什么要有once:并发情况下,期望某函数只被执行一次
// once可以被用来做什么:初始化配置、初始化实例(单例模式)、保持数据库连接等
type Student struct {
Name string
Age int
}
//为什么该方法不正确?????????
//var s *Student
//
//func initialize() {
// s = &Student{
// Name: "gss",
// Age: 18,
// }
//}
//
//func main() {
//
// var once sync.Once
// once.Do(initialize())
// fmt.Printf("%#v\n", s)
//}
var once sync.Once
var s *Student
func newInstance(wg *sync.WaitGroup) *Student {
defer wg.Done()
once.Do(func() {
s = &Student{
Name: "33",
Age: 18,
}
fmt.Println("init")
})
return s
}
func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go newInstance(wg)
}
wg.Wait()
fmt.Printf("%#v\n", s)
}
3.2 init函数:也只执行一次
在一个 go 的包(package)中,全局变量与函数一样,不管在哪个 go 代码文件中声明,名称是全包内唯一,不能重复的。init 是是 go 中唯一可以在一个包内的不同代码文件中多次定义的函数名称(即唯一一种可在包内重复命名,一个包中可以有多个init函数
init不能被调用(不需要被调用执行),而是会按照const、var、init、main顺序直接被隐式加载执行。
init函数不接受任何参数,也没有返回值
由于在程序初始时就会被执行,故假如init所加载的函数变量久久未被使用到,则会浪费内存;而once则是在使用到的时候才初始化加载
https://zhuanlan.zhihu.com/p/34211611
https://www.lanseyujie.com/post/golang-init-and-sync-once-difference.html
4、sync.Pool:并发安全的临时对象池
为什么要有Pool:比如网络通信时,客户端收到结果,go需要unmarshal序列化服务器返回的结果,到临时结构体变量中。当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。
作用:
作为可复用的对象池。池中的对象通过Get方法获取,通过Put方法返入池中。
各个groutine之间可以共享这些临时对象
注意:
存放在池中的对象如果不活跃了会被自动清理,是被不被通知的释放
package main
import (
"fmt"
"sync"
)
type Student struct {
Name string
Age int
}
var pool = &sync.Pool{
New: func() interface{} {
//new:初始化基本数据类型的内存。参数必须为一个基本数据类型,new返回一个指向该类型的指针,即返回的是地址
//new:会申请内存并将值清0
//make:初始化 一些引用类型(变量存储的本身就是一个地址)的 内存,如slice、map、chan
return new(Student)
},
}
func main() {
s1 := pool.Get().(*Student)
fmt.Printf("s1 = %#v, s1 addr = %v\n", s1, &s1)
fmt.Println(s1)
fmt.Println(*s1)
//defer pool.Put(s1)
s1.Name = "gss"
s1.Age = 18
fmt.Printf("now s1 = %#v\n", s1)
fmt.Printf("%v,%p\n", s1, &s1)
//将s1放回临时对象池,此时不一定会放入原来s1所在的内存地址,可能s1被放入别的内存地址,故取出的s2地址变了
pool.Put(s1)
//Get从临时对象池取出对象,该对象会从pool中移除,返回给调用者。注意,Get返回接口类型,需要进行断言,即指针类型
//假如pool中没有更多的空闲元素可返回时,就会调用 New 方法来创建新的元素;如果未设置 New,当无更多的空闲元素可返回时,Get 方法将返回 nil,表明当前没有可用元素,故Get方法需要用户判断返回值是否为 nil
s2 := pool.Get().(*Student)
fmt.Printf("s2 = %#v, s2 addr = %v\n", s2, &s2)
}
5、sync/atomic
原子操作(执行不会被中断),无须加锁
二、管道
不是通过共享内存来通信,而是通过通信来共享内存
第一版:
因切片不支持并发,故
协程2 写管道
协程1 for select等待读管道,将读出的值写入最终的切片
waitgroup只等了携程2执行结束
故主线程和协程1并行执行了,主线程结束,管道写切片还没写完
改为:
for range读一个关闭的管道,读完就会退出。若管道没有被关闭,for range就会阻塞
管道关闭后仍可以读,只不过不能写
for里开协程,并发 把所有结果写到最终的切片里,因为不能并发,所以引入管道
1、无缓冲管道:
package main
import (
"fmt"
"time"
)
func writeChan(ch chan int) {
j := 6
fmt.Println(j)
//如果没有接收groutine,则阻塞在这个for,只能写进去0,然后函数一直停在这里
for i := 0; i < 4; i++ {
fmt.Printf("i = %v\n", i)
ch <- i
}
close(ch)
}
func readChan(ch chan int) {
j := 7
fmt.Println(j)
//for range可以读一个被关闭的管道,若管道为空且没有关闭,for range就会阻塞,但因为main groutine执行,该阻塞会被退出从而打印end
for data := range ch {
fmt.Printf("data = %v\n", data)
}
}
func main() {
//管道:用来在groutine之间通信(跨进程时channel无法通信)
//无缓冲管道:必须在接收方准备好后,发送方才能发送成功,否则发送方阻塞在发送,无法继续发送成功。
// 即无缓冲管道是同步收发,信必须交到收件人手上,快递员才能离开
c := make(chan int)
go writeChan(c)
go readChan(c)
time.Sleep(6 * time.Second)
fmt.Println("end")
}
2、带缓冲管道
package main
import (
"fmt"
"time"
)
func writeChan(ch chan int) {
j := 6
fmt.Println(j)
for i := 0; i < 4; i++ {
ch <- i
fmt.Printf("i = %v\n", i)
}
//close(ch)
}
func readChan(ch chan int) {
for data := range ch {
fmt.Printf("data = %v\n", data)
}
}
func main() {
//带缓冲管道:不用有接收方,即可写成功
//发送阻塞只会出现在以下情况:
//1、往通道发送数据时,只有当缓存满时,发送方才会阻塞(此时,main groutine也会继续执行,直到右括号main结束,发送方阻塞也被结束)
c := make(chan int, 2)
go writeChan(c)
go readChan(c)
time.Sleep(6 * time.Second)
//2、从通道接收数据时,只有当缓存为空时,接收方才会阻塞(此时缓存为空,该语句被阻塞,永远也执行不到打印end)
data := <-c
fmt.Println(data)
//3、容量与长度
//第2个参数=容量,即能容纳的最大数据个数, cap(c3)
//长度,当前时刻缓存的数据个数, len(c3)
c3 := make(chan string, 2)
c3 <- "gss"
fmt.Printf("chan cap = %d\n", cap(c3))
fmt.Printf("chan len = %d\n", len(c3))
fmt.Println("end")
}
结果:
6
i = 0
i = 1
i = 2
data = 0
data = 1
data = 2
data = 3
i = 3
chan cap = 2
chan len = 1
end