语言进阶——并发编程
并发VS并行
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
go可以充分发挥多核优势,高效运行
协程VS线程
协程——用户态
用户态,轻量级线程,栈KB级别
线程——内核态
内核态,线程跑多个协程,栈MB级别
线程上可以并发的跑多个协程
进程
进程是应用程序的启动实例,是系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间,不同进程通过进程间的通信方式来通信
线程
线程从属于进程,是进程中的一个实体,线程是CPU调度的基本单位,一个线程由线程ID、当前指令指针、寄存器集合和堆栈组成
线程不拥有自己的系统资源,它与同属于同一进程的其他线程共享进程所拥有的全部资源,多个线程之间通过共享内存等线程间的通信方式来通信,线程拥有自己独立的栈和共享的堆
协程
协程可以理解为轻量级线程,一个线程可以拥有多个协程,与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程,这也就是GO在语言层面直接支持协程的含义
⭐进程的切换内容包含页全局目录、内核栈、硬件上下文,切换内容保存在内存中
⭐线程的切换内容包含内核栈、硬件上下文,切换内容保存在内核栈中
⭐协程的切换内容是硬件上下文,切换内容保存在用户栈或堆中
在实际过程中如何开启协程
只要在创建的函数前面加个go,就可以开启协程了
//快速打印hello goroutine: 0~hello goroutine:4
func hello(i int) {
printfln("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
协程的执行过程
每一个go里面的线程会从schedule()开始运行,并且schedule()方法开始时是在g0栈上执行,g0栈就是给g0协程在栈空间中分配的内存地址,用来记录函数调用、跳转的信息。
G0 是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0
协程如何实现通信
提倡通过通信共享内存而不是通过共享内存而实现通信
Channel
make(chan 元素类型,【缓冲大小】
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2)
waitgroup
计数器:
开启协程+1;执行结束-1;主协程阻塞直到计数器为0
语言进阶——依赖管理
依赖的概念:编译程序不可能所有东西都自己写,我们会大量使用一些第三方的库来引入自己的代码
演进:
gopath
- go语言支持的一个环境变量 $GOPATH
- 项目代码直接依赖src下的代码
- go get 下载在最新版本的包到src目录下
弊端:无法实现package的多版本控制
go vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor =>GOPATH
- 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
弊端:
无法控制依赖的版本
更新项目又可能出现依赖冲突,导致编译出错
go module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
- 终极目标:定义版本规则和管理项目依赖关系
go mod命令
| 命令 | 作用 |
|---|---|
| go mod init | 生成go.mod文件 |
| go mod download | 下载go.mod文件中指明的所有依赖 |
| go mod tidy | 整理现有的依赖 |
| go mod graph | 查看现有的依赖结构 |
| go mod edit | 编辑go.mod文件 |
| go mod vendor | 导出项目所有的依赖到vendor目录 |
| go mod why | 查看为什么需要依赖某模块 |
| go mod verify | 校验一个模块是否被篡改过 |
依赖管理三要素:
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 proxy
- 本地工具 go get/mod
当同时有两个版本时(兼容),最终执行的是版本低的那个
依赖分发-回源
- 无法保证构建稳定性——增加/修改/删除软件版本
- 无法保证依赖的可用性——删除软件
- 增加第三方压力——代码托管平台负载问题
测试
- 单元测试
- Mock测试
- 基准测试
测试是避免事故的最后一道屏障
测试一般分为:
- 回归测试:A同学手动通过终端回归一些固定的主流程场景
- 集成测试:对系统功能维度做测试验证
- 单元测试:开发者对单独的函数、模块做功能验证
单元测试
规则:
- 所有测试文件以 test.go 结尾
func TestXxx( *testing.T)- 初始化逻辑放到 TestMain 中
eg:
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
运行结果:
ok github.com/Moonlight-Zhao/go-project-example/test (cached)
覆盖率
衡量代码是否经过了足够的测试;
评价项目的测试水准;
评估项目是否达到了高水准测试等级
eg:
判断是否及格的函数:
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
运行结果:
ok command-line-arguments 0.315s coverage: 66.7% of statements
依赖
⭐外部依赖 => 稳定&幂等
实际功臣中复杂的项目,一般依赖较多,而单元测试需要保证稳定性和幂等性
稳定性:相互隔离,能在任何时间、任何环境运行测试
幂等性:每一次测试运行都应该产生与之前一样的结果
| 函数 | 说明 |
|---|---|
| ReadFirstLine() | 文件处理的函数 |
Mock
使用 Monkey 库,Monkey 是一个开源的 mock 测试库,可以对方法或者实例进行 mock
monkey:github.com/bouk/monkry
🍇快速Mock函数:
- 为一个函数打桩
- 为一个方法打桩
eg:对 ReadFirstLine 函数打桩测试,不在依赖本地文件
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
基准测试
基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。在实际项目开发中经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。
- 优化代码,需要对当前代码进行分析
- 内置的测试框架提供了基准测试的能力
eg:随机选择执行服务器
import(
"math/rand"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
基准测试以 Benchmark 开头,输入参数为*testing.B,Parallel为多协程并发测试
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
可以看到代码在并发情况下存在劣化,主要原因是 rand 为了保证全局的随机性和并发安全,持有一把全局锁。解决这个问题——使用开源的高性能随机数方法 fastrand