这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
本篇笔记主要分享一些第二天课程的知识和理解吧
Part 1
Go语言所支持的goroutine协程是非常高效率的,当然它的并发也有并发常见的脏数据问题。
比如说Go早期的map并不是并发安全的,而后面慢慢推出的比如 Sync.map 就是在map层上加锁,而后面也有类似ConcurrentHashMap这种分段加锁实现并发安全的map。
当然Go主推的是通过通信分析内存,说到通信就要提到Go中的channel了。
channel分为有缓存和无缓存的channel,分别对应异步和同步。如果使用的是无缓存的channel,一定要注意死锁情况的发生。
一定要注意的是:
给一个nil的channel发送数据,会造成死锁。
从一个nil的channel接收数据,会造成死锁。
给一个已经关闭的channel发送数据,会造成panic错误。
从一个已经关闭的channel接收数据,如果缓冲区为空,则会返回一个零值。
相信根据上述的说法可以发现channel是服从先入先出FIFO原则的。如果说是有缓存的channel,底层应该会通过ring buffer实现。
例如缓冲区为8的一个channel buffer。recvx指向最早被读取的数据,而sendx指向再次写入时插入的位置。
而如果chan只是需要传递一种信息,推荐使用空结构体。这是因为空结构体不占内存。非常适合做占位符,而且定义空的结构体时,返回的地址都是一样的,不会去占用其他空间。
课程中还有使用waitgroup计数器进行线程同步,可以通过计数器来操控多个goroutine的结束。每一次Add执行,请求计数器v +1,而Done方法执行,等待计数器w -1,而v为0时通过信号量唤醒Wait()。从而执行接下来的代码。常用于阻塞主线程,避免因为主线程提前结束从而导致goroutine工作未完成而被迫结束。
关于并发之类的东西,还是需要了解Go的Sync包,比如适合单例模式的Sync.Once,降低GC压力的Sync.Pool等等。
Part 2
因为接触Go的语言比较早,大概在1.14左右版本,那时候go mod好像还没有在2019版的goland支持,那时候需要进行编程个人一般都是放在GOPATH下,但是这样管理代码有些麻烦,因为对于依赖包而言几乎是都要全部下载在同一个文件夹底下的,个人现在的GOPATH下还是保存当初的样子。
在GOPATH下的代码通过go get获取之后都会安装到相应文件夹内,这时候会有一些弊端的出现,有时候需要使用的依赖版本问题,需要去修改文件夹名称,比较繁琐。
而在go module之后,在项目下只需要 go mod init 项目名; go mod tidy; 两行代码就可以把依赖打包好,非常方便。
Part 3
分享一个经典问题,开2个goroutine,1个goroutine打印数字,1个goroutine打印字母
输出样例:
1,2,A,B,3,4,C,D,5,6,E,F,7,8,G,H,9,10,I,J,11,12,K,L,13,14,M,N,15,16,O,P,17,18,Q,R,19,20,S,T,21,22,U,V,23,24,W,X,25,26,Y,Z,
package main
import (
"fmt"
"sync"
)
// 交替打印数字和字符
var numc = make(chan struct{})
var bytec = make(chan struct{})
var num int = 1
var wg sync.WaitGroup
func main() {
wg.Add(1)
go printNum()
go printByte()
numc <- struct{}{}
wg.Wait()
}
func printNum() {
for {
select {
case <-numc:
fmt.Printf("%d,%d,", num, num+1)
bytec <- struct{}{}
//break
}
}
}
func printByte() {
for {
select {
case <-bytec:
fmt.Printf("%c,%c,", 'A'+num-1, 'A'+num)
num += 2
if num >= 26 {
wg.Done()
return
}
numc <- struct{}{}
//break
}
}
}
其中numc和bytec分别传递信息给函数进行打印,然后通过waitgroup进行主线程的阻塞控制等待即可。
在打印数字之后往chan中传递信息启动另一个函数,以此重复执行即可完成需求。
人生苦短,不如go浪一下。