go语言圣经读书笔记(下)

协程和管道

协程

进程线程协程区别

程序与进程,程序是写的代码生成的二进制文件,是一个静态的概念,写好了就放在那了,而进程是一个动态的概念,同一个进程享有同一份资源,进程是程序运行时的描述,可以分为5种状态。创建态,就绪态,执行态,阻塞态,终止态,就是程序开始后就交给操作系统来管理以进程的方式。

线程,在程序执行中有时要面临多个任务,老式程序中只能顺序执行,视频时不能发消息,现在程序可以利用线程来解决这种问题,一个进程中可以含有多个线程,线程之间可以并发,从而达到边发消息边视频的功能。引入线程后,cpu就以线程为单位来进行调度,当切换到同进程的线程时,因为他们之间共享资源,切换时开销比较小,切换到别的进程的线程时才需要大量开销,总体还是减少了开销,提高了效率,并使得一个进程可以同时完成多个任务。

协程也是为了提高性能,线程遇到问题无法执行,要被替换,但是如果,给线程多个工作,一个执行不了被阻塞可以换其他工作来执行,不必切换抵消了切换的开销。而这多个工作就叫协程,协程间的切换就更小了,不涉及下处理器(cup),协程是编程概念的,程序员操作,进程线程是操作系统概念,由操作系统来管理。

1 进程是资源分配的单位 
2 线程是操作系统调度的单位 
3 进程切换需要的资源很最大,效率很低 
4 线程切换需要的资源一般,效率一般 
5 协程切换任务资源很小,效率高 
6 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程。

详见Go语言——线程、进程、协程的区别_go线程线程进程-优快云博客

管道

协程之间可以来回切换实现高并发,还有各个goroutine之间的通信问题,线程之间通信常用手段,共享内存,消息传递,管道通信。go通信模型基于csp哲学,不推荐使用共享内存,而使用管道进行通信,此管道不是操作系统提供的管道,是go自己实现的数据结构。chan实现多个goroutine通信,所以chan一定是线程安全的,底层通过锁来实现。

双向的chan
操作nil的chan正常chan关闭的chan
<- ch阻塞空阻塞,非空正常读到零值

ch<-

阻塞满阻塞,非满正常panic
close(ch)panic成功panic

无缓冲 channel 在读和写的过程中是都会阻塞,无缓冲阻塞到读写同时进行。

单向的chan,只读只写两种,见名知意另一种操作被封锁,使用报错。
关于select
select采用多路复用思想,通过多个case监听多个Channel的读写操作,任何一个case可以执行则选择该case执行,否则执行default。如果没有default,且所有的case均不能执行,则当前的goroutine阻塞。对于多个chan传递的信息,可以使用一个进程来进行处理,本来多个进程处理的事情使用一个进程就叫做多路复用,select使用一个协程同时监视多个chan信号,提高信息处理效率。
针对select中case数量的不同,编译器会对生成代码进行优化,针对不同情况生成不同函数来调用。最常见情况是多个case情况,这时生成的函数叫selectgo逻辑是,1.根据规则生成遍历顺序和加锁顺序并加锁,2,根据遍历顺序检查各case对应chan的缓冲队列情况,3,没有可行情况则检测是否有default,有执行,没有则给所以chan解锁,把该协程g加入到各chan对应的发送或接收等待队列并阻塞当前进程等待唤醒。4、等有chan可行了,会唤醒该进程g并完成对应分支操作。5,对各chan加锁,在个chan发送或接收队列中移除自身,然后按序解锁后返回。

基于共享变量的并发

竞争条件

竞争条件,通过方法实现修改变量balance的函数,Balance方法和deposit方法,Deposit传入参数amount来修改balance+=balance,Banlance来得到balance的值。修改值分三步,取到寄存器,修改寄存器,放回原地址空间。不同协程同时调用这些方法时候,三个步骤之间交错进行,可能的不到我们希望的结果,操作系统的经典案例。

基于这个背景,衍生出来两种数据结构,锁,读写锁。

sync.Mutex互斥锁

sema <- struct{}{} // 无缓冲的管道,其他协程同样操作,可确保只有一个协程继续下去
balance = balance + amount //进行有风险操作,可以确保无人同步操作,这一部分叫临界区
<-sema // release token //操作完之后释放这个管道,供其他人使用。

这种方式被sync.Mutex类型直接支持,lock方法就是入管道,进入临界区,unlock方法退出临界区出管道。

sync.RWMutex读写锁

读写互斥锁(RWMutex)是一种特殊类型的互斥锁,它允许多个协程同时读取某个共享资源,但在写入时必须互斥,只能有一个协程进行写操作。相比互斥锁,读写互斥锁在高并发读的场景下可以提高并发性能,但在高并发写的场景下仍然存在性能瓶颈。

关于Banlance之内为什么需要加锁,仅仅只是一个读取操作不修改值为什么需要加锁?

这个就关系到计算机组成,现代计算机是多cpu的,每一个cpu都有缓存,手写代码逻辑看似没有问题,修改后数据回缓存在本地的缓存中,写入主存顺序不同可能导致跟我们实际的逻辑结果不一致。比如这段代码可能会输出x:0 y:0或y:0 x:0,协程a两条指令不直接相关,a协程以为谁前谁后都一样,编译时两条命令的顺序就不确定了,协程b同理,所以就导致了均为0的情况出现。

var x, y int
go func() {
    x = 1 // A1
    fmt.Print("y:", y, " ") // A2
}()
go func() {
    y = 1                   // B1
    fmt.Print("x:", x, " ") // B2
}()

通过锁机制可以保证一系列操作顺序进行,防止协程并发执行造成未知错误。

惰性初始化

背景:有一些变量如果在声明时就赋值变量,程序启动就会特别慢,一般一些变量会在使用时是否为空,空着初始化它再使用。可能有人认为 不同协程检测icons时会多次初始化icons导致的问题,这是不对的,问题是缺少显示的同步,导致编译器和CPU是可以随意地去更改访问内存的指令顺序,以任意方式,只要保证每一个goroutine自己的执行顺序一致。可能出现的情况是先返回值返回空再初始化,即是map中有需要的值也会返回空的逻辑错误。
var mu sync.RWMutex // guards icons
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    mu.RLock()            //若已经初始化完可以直接拿到值,通过读锁即可,性能高一点也
    if icons != nil {
        icon := icons[name]
        mu.RUnlock()
        return icon
    }
    mu.RUnlock()

    // acquire an exclusive lock
    mu.Lock()                   //map不为nil则需要初始化,读锁就不可以了,互斥锁同步。
    if icons == nil { // NOTE: must recheck for nil
        loadIcons()
    }
    icon := icons[name]
    mu.Unlock()
    return icon
}

这种方式比较复杂,go提供了更好的方式 

var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

sync.Once.Do传入一个函数,只会执行这个函数一次,sync.Once里面是一个布尔变量一个互斥变量,用来标志是否是初始化一次,与init函数类似,init是在程序加载时执行初始化,sync.Once是在需要的时候来进行初始化。 

测试

测试用法

_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分,通常都是《待测试文件名_test.go》文件来进行测试,通过go test命令来执行测试文件。

测试文件必须导入testing包,测试文件中函数命名还有要求,测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,如TestLog,测试函数的参数有三种分别是t *testing.T,*testing.B,*testing.M对应不同的测试方式。

  1. 运行 go test,该 package 下所有的测试用例都会被执行。
  2. go test -v-v 参数会显示每个用例(测试函数)的测试结果
  3. go test -run TestAdd -v测试指定用例

子测试是 Go 语言内置支持的,可以在某个测试用例中,用法 go test -run TestMul/pos -v

帮助函数

在测试文件中处理测试函数也就是测试用例之外还可以有帮助函数,在用例中调用,辅助测试用例使用。帮助函数例子

// calc_test.go
package main

import "testing"

type calcCase struct{ A, B, Expected int }

func createMulTestCase(t *testing.T, c *calcCase) {
	// t.Helper()     
	if ans := Mul(c.A, c.B); ans != c.Expected {
		t.Fatalf("%d * %d expected %d, but %d got",
			c.A, c.B, c.Expected, ans)
	}

}

func TestMul(t *testing.T) {
	createMulTestCase(t, &calcCase{2, 3, 6})
	createMulTestCase(t, &calcCase{2, -3, -6})
	createMulTestCase(t, &calcCase{2, 0, 1}) // wrong case
}

代码中注释了t.Helper(),没有helper报错时报在帮助函数内不方便观察出错地方,有了t.Helper即使是帮助函数内部错误,报错显示的调用他的函数方便确定位置。

setup,teardown和TestMain

func setup() {
	fmt.Println("Before all tests")
}

func teardown() {
	fmt.Println("After all tests")//这俩特殊函数可用用来执行关闭网络连接,释放文件等功能
}

func Test1(t *testing.T) {
	fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
	fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {//前面提到的一种测试方式,m.Run执行所有用例,
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

基准测试

  1. 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
  2. 参数为 b *testing.B
  3. 执行基准测试时,需要添加 -bench 参数。
func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}

基准测试主要用来测试性能,测试结果结构体

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

反射

反射主要是用于在运行时获取变量的类型信息、值信息、方法信息等等。

//反射包基本使用。
//反射基本的数据结构,Value,Type
//每一个变量对应value和type,通过反射可以生成对应的Value,Type用来操作这个变量
var a = 1
t := reflect.TypeOf(a)   //返回值类型为Type类型,通过反射包里的TypeOf方法得到

var b = "hello"
t1 := reflect.ValueOf(b)  //返回值类型为Value类型,通过反射包里的ValueOf方法得到

关于Value类型

  • 设置值的方法:Set*SetSetBoolSetBytesSetCapSetComplexSetFloatSetIntSetLenSetMapIndexSetPointerSetStringSetUint。通过这类方法,我们可以修改反射值的内容,前提是这个反射值得是合适的类型。CanSet 返回 true 才能调用这类方法
  • 获取值的方法:InterfaceInterfaceDataBoolBytesComplexFloatIntStringUint。通过这类方法,我们可以获取反射值的内容。前提是这个反射值是合适的类型,比如我们不能通过 complex 反射值来调用 Int 方法(我们可以通过 Kind 来判断类型)。
  • map 类型的方法:MapIndexMapKeysMapRangeMapSet
  • chan 类型的方法:CloseRecvSendTryRecvTrySend
  • slice 类型的方法:LenCapIndexSliceSlice3
  • struct 类型的方法:NumFieldNumMethodFieldFieldByIndexFieldByNameFieldByNameFunc
  • 判断是否可以设置为某一类型:CanConvertCanComplexCanFloatCanIntCanInterfaceCanUint
  • 方法类型的方法:MethodMethodByNameCallCallSlice
  • 判断值是否有效:IsValid
  • 判断值是否是 nilIsNil
  • 判断值是否是零值:IsZero
  • 判断值能否容纳下某一类型的值:OverflowOverflowComplexOverflowFloatOverflowIntOverflowUint
  • 反射值指针相关的方法:AddrCanAddrtrue 才能调用)、UnsafeAddrPointerUnsafePointer
  • 获取类型信息:TypeKind
  • 获取指向元素的值:Elem
  • 类型转换:Convert

Len 也适用于 slicearraychanmapstring 类型的反射值。

关于Type类型(一个接口对象)含有常用通用的方法如下:

string()返回变量对应类型,返回myint

kind()返回底层类型,返回int

Implements(u Type)是否实现了接口u

Comparable()是否可比

Method(int) Method传入数字返回方法,通常for遍历来使用

NumMethod()变量可使用变量数量

等等

还有一些变量含有特殊的方法如果类型不对,则回报panic

ChanDir()返回通道方向,chan专用

IsVariadic()是否为可变参数,func专用

NumField() int返回包含属性数,结构体专用

Field(i int)返回第i个属性,结构体专用

FieldByIndex(index []int)StructField结构体里还是结构体,index拨开访问,结构体专用

FieldByName(name string) (StructField, bool)寻找给定名字的属性,返回是否找到

Key() Type返回map的key类型,map专用

NumIn() int返回函数参数数,func专用

NumOut() int返回值数,func专用

Len() int数组长度,数组专用

详见:深入理解 go reflect - 反射基本原理 - 掘金 (juejin.cn)

反射灵活但是耗费性能很大,bug不好找,能不用尽量不用反射。

底层编程

编写一些实实在在的应用是真理。请远离reflect和unsafe包,除非你确实需要它们。

书末尾这样写到。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值