Go专家编程读书小记

news2024/11/26 19:50:25

文章目录

  • 协程
      • 进程和线程
      • 进程:
      • 进程间通信:
      • 线程:
      • 区别:
      • 协程
      • GMP模型
      • 调度策略
  • 内存管理
      • 内存分配
      • span:
      • cache:
      • central:
      • heap:
      • 垃圾回收
      • 常见的垃圾回收算法:
      • 三色标记:
      • 垃圾回收优化:
      • GC流程:
      • 逃逸分析:由编译器决定内存分配的位置
      • 编译器根据对象是否被函数外部引用来决定是否逃逸:
  • 并发控制
      • 控制方式
    • Channel:
    • WaitGroup:
      • Add:
      • Wait:
      • Done:
    • context:
      • emptyCtx:
      • cancelCtx:
      • timerCtx:
      • valueCtx:
      • 锁机制
        • Normal模式
        • Starving模式
      • Woken模式
      • 自旋
        • 自旋条件:自旋并不是无限制的,只有在不忙的时候才会自旋
      • 读写锁
      • 写操作如何阻止读操作:
      • 读操作如何阻止写操作:
      • 写锁定不会饿死吗:
  • 反射
  • 异常处理
      • errors包
      • defer
      • panic
      • recover
  • 定时器
      • Timer
      • Ticker
      • timersBucket
  • 测试
      • 单元测试
      • 性能测试
      • 示例测试
      • 子并发测试
      • Main测试
      • common
      • TB、T、B

首先书很好看,值得去读,其中的原理通俗易懂,配合着源码详略都有,对以前一些较模糊难懂的概念也有了自己的认识和理解

协程

进程和线程

进程:

  • 拥有资源分配的基本单位
  • 进程是一个独立的单位,有独立的内存空间

进程间通信:

  • 管道:父子进程间通信
  • 信号:中断机制的一种模拟
  • 消息队列:消息链接表,具有写权限进程可以按照一定规则向消息队列中添加新信息;具有读权限进程则可以从消息队列中读取信息
  • 共享内存:多个进程可以访问同一块内存空间,可以看到对方共享内存中数据的更新,依靠同步
  • 信号量:进程间或同进程间不同线程间的同步和互斥手段
  • 套接字:用于网络中不同机器间进程通信

线程:

  • 调度和分配的基本单位
  • 共享进程资源内存等方式线程间通信
  • 线程可以创建和取消另一个线程,进程间线程并发执行
  • 易调度,开销少,可并发,创建线程比创建进程开销少,充分利用多处理器的功能

区别:

  • 一个 线程只能属于一个进程,而一个进程可以有多个线程,至少一个线程
  • 资源分配给进程,同一进程的线程共享资源
  • 真正执行处理的是线程,进程内的一个执行单位,可调度实体
  • 线程不拥有系统资源,但可以访问隶属于进程的资源
  • 分配和回收资源时,进程开销明显大于线程

协程

协程是一种更轻量级的线程,不受操作系统直接调度,而是由用户应用程序提供的协程调度器按照调度策略调度到线程执行

Go协程调度器由runtime包提供,go关键字创建协程,语言层面直接支持协程

高并发应用中频繁创建线程难免造成不必要的开销,所以可以线程池保存一定的线程,新的任务不再是以创建线程执行,而是加入到任务队列。线程池中的线程不断地从任务队列中取出执行,就避免了频繁地创建和销毁线程。

线程池的线程毕竟有限,如果一个线程阻塞一个任务,任务队列中任务就会堆积,可以通过创建线程来缓解这个问题,但是线程过多会造成竞争CPU资源

过多的线程上下文切换开销大,用户态的协程可以减少切换开销,协程调度器可以把可调度的协程给线程执行,同时把阻塞的协程调度出协程,避免线程的频繁切换实现高并发

GMP模型

Go实现的是M:N模型,也就是M个用户线程运行在N个内核线程中,协程上下文切换快,调度算法复杂

  • goroutine(G):go关键字创建协程
  • machine(M):内核线程,必须有P才能执行,M个数稍大于P
  • processor§:协程调度器,默认是CPU核数

M阻塞执行G,其他G等待被调用,全局G队列多个处理器共享
协程G创建的子协程会被加入到本地队列,队列满了则加入到全局,同样的,调度器P除了调度本地队列,也会周期的去调用全局G

在这里插入图片描述

调度策略

  • 轮转队列:P从本地G队列中取出到M执行,也会周期的从全局队列中调度G,避免饿死
  • 系统调用:前面说了M稍多余P,M和P又需要绑定才能执行,那多余的M就是在系统调用时起作用。比如,M0释放掉P,多余的M1代替M0继续和P工作,从队列中获取协程,M0陷入系统调用继续执行G。G执行完毕如果有空闲的P则M0获取,如果没有M0就休眠
  • 工作量窃取:P维护的协程数量是不均衡的,当某个P没有协程时会先检查全局G,没有再从其他P窃取协程过来,每次偷一半
  • 抢占式调度:类似时间片轮转,避免某个协程长时间执行

内存管理

内存分配

申请的内存会被划分成三个部分

  • arena:即堆区,应用需要的内存也是从这里分配出去的,这个区域会划分成 page页,每个页8KB
  • spans:存放span指针的区域,每个指针又对应一个或多个page
  • bitmap:和GC标志有关

在这里插入图片描述

span:

用来管理arena中page页的,包含一个或多个连续页,页又会根据需要划分出更小的粒度class,每个span管理特定的大小的class对象

type mspan struct {
	next *mspan     // 链表前向指针
	prev *mspan     // 链表后向指针
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // 管理的页开始地址
	npages    uintptr // 页数

	manualFreeList gclinkptr // list of free objects in mSpanManual spans
  nelems uintptr // 块个数
  allocBits  *gcBits		// 每一位代表一个块是否被分配
	gcmarkBits *gcBits		// 每块的gc标记情况
	allocCache uint64
  sweepgen              uint32
	allocCount            uint16        // 已被分配的块数
	spanclass             spanClass     // size class and noscan (uint8)
	state                 mSpanStateBox // 
	needzero              uint8         // needs to be zeroed before allocation
	allocCountBeforeCache uint16        // 
	elemsize              uintptr       // 块大小
	limit                 uintptr       // 
	speciallock           mutex         // guards specials list
	specials              *special      // 
}

在这里插入图片描述

cache:

为了避免多线程申请内存时频繁加锁,为每个线程分配了span的缓存

数组中每个元素代表一种class类型的span列表,列表又有两种,一种包含了指针,一种不包含指针,目的是为了方便GC扫描,没指针对象的就没必要去扫描他

type mcache struct {
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
	stackcache [_NumStackOrders]stackfreelist
}

在这里插入图片描述

central:

用来管理span,这么说吧,cache是为了单个线程提供服务缓存,central则是为了多线程服务

当某个线程内存不足时向central申请,当线程释放又回收到central,cache初始时是没有span的,过程中动态的从central获取并缓存下来

  • 申请span过程:加锁、从空闲表中取span并从链表中删除加入到非空闲链表中,span返回给线程,解锁、线程将span缓存到cache
  • 释放span过程:加锁、从非空闲链表删除加到空闲链表,解锁
type mcentral struct {
	spanclass spanClass
	partial [2]spanSet // 还有空闲块的span列表
	full    [2]spanSet // 没有空闲块的span列表
}

heap:

知道了central管理的某一种大小的class类型的span,这些central又存放在 heap中,Go也就是通过一个heap管理内存

type mheap struct {
	lock mutex
  allspans []*mspan // all spans out there
  
  curArena struct {	// 当前arena区域的起始和已使用的地址
		base, end uintptr
	}
  
  central [numSpanClasses]struct { //  每种类型class对应的central
		mcentral mcentral
		pad      [cpu.CacheLinePadSize -unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}
}

垃圾回收

常见的垃圾回收算法:

  • 引用计数:每个对象维护一个计数,引用该对象的对象被销毁则减1,直到0就回收对象
    • 比如:Python、PHP
    • 好处就是能够很快的发现并回收,不会等到内存耗尽才回收,缺点是不能很好处理循环引用,实时维护计数也要代价
  • 分代收集:按照对象生命周期长短划分不同的代空间,也就是老年代、新生代,不同代有不同的回收算法和频率
    • 比如:Java
    • 好处就是回收性能好,但是算法复杂
  • 标记清除:从根变量开始遍历引用的对象,引用的标记为“被引用”,没有的就会被回收
    • 比如:Go三色标记
    • 好处是避免了引用计数的循环引用问题,但是需要STW(停止所有协程,专心做垃圾回收)

三色标记:

  • 白色:对象未标记(gcmarkBits 对应位 0)
  • 灰色:对象在标记队列中等待
  • 黑色:对象被标记(gcmarkBits 对应位 1)

垃圾回收优化:

因为STW的存在,STW 的时间长短直接影响了应用的执行,为了缩短STW时间,需要优化垃圾回收

  • 写屏障:允许goroutine与GC同时运行减弱STW,GC过程中新分配的内存会被立即标记,不会在本轮回收

    • 灰色Dijistra插入写屏障(白色先标记为灰色,避免被黑色引用,所有的白色均被灰色引用,重扫就不会有遗漏)

    • 黑色Yuasa删除写屏障(标记为灰色,创造灰色到灰色或灰色到白色的路径)

    • 混合Hybrid写屏障(Go1.8以后简化GC流程,减少标记重扫的成本,对正在被覆盖的对象着色,且如果当前栈未扫描完成,则同样对指针着色)

  • 强三色不变性:黑色不允许引用白色

  • 弱三色不变性:黑色可以引用白色,但白色有其他灰色间接引用

在这里插入图片描述

GC流程:

在这里插入图片描述
在这里插入图片描述

逃逸分析:由编译器决定内存分配的位置

  • 如果分配在栈中,函数执行结束自动内存回收
  • 如果分配在堆中,函数执行结束可交给GC处理

编译器根据对象是否被函数外部引用来决定是否逃逸:

  • 没被引用,优先放入栈中
  • 被引用,一定是放到堆中

还有当内存过大超过栈存储能力时,也会放到堆中

  • 动态类型逃逸
func main(){
  s := "test"
  fmt.Println(s)
  // Println(a...interface{}),编译期间很难确定参数具体类型,也会产生逃逸
}
  • 闭包引用
func fibonaci() func() int {
  a,b := 0,0
  return func() int {
    a,b = b,a+b
    return a
  }
  // 闭包引用了局部变量a、b,所以被放入到堆中
}

所以有时候函数指针传递不一定就比值传递好,虽然可以提高效率但是会发生逃逸,加剧了GC的负担

并发控制

控制方式

实际中经常遇到这种场景,协程A创造出多个子协程,然后就需要等待协程退出,Go也有三种方式解决这个问题

  • channel 实现简单
  • waitgroup 协程个数可以动态调整
  • context 可以很好的操作派生出来的子孙协程

Channel:

有优点有缺点,优点就是实现简单,缺点是当需要大量创建协程时需要同样数量的Channel,而且对子孙协程不好控制

func main() {
	channels := make([]chan int, 10)
	for i := 0; i < 10; i++ {
		channels[i] = make(chan int)
		go process(channels[i])
	}
	for i, ch := range channels {
		<-ch
		fmt.Println("子协程", i, "退出")
	}
}
func process(ch chan int) {
	time.Sleep(time.Second)
	ch <- 1
}

WaitGroup:

通过信号量的方式控制子协程,信号量 >0 资源可用,获取后自动减1,信号量 = 0 资源不可用,线程睡眠

缺点是子协程派生新的协程时,goroutine数量不太好确定,但WaitGroup Add方法需要提前预知

type WaitGroup struct {
	noCopy noCopy		// 锁
	state1 uint64		// 两个计数器,counter当前还未执行结束的goroutine计数器,waiter等待goroutine结束的goroutine数量
	state2 uint32		// 信号量
}

提供了三个接口函数 Add、Wait、Done

Add:

func (wg *WaitGroup) Add(delta int) {
	statep, semap := wg.state()		// 获取到 state1 和 state2 指针
	if race.Enabled {
		_ = *statep // trigger nil deref early
		if delta < 0 {
			race.ReleaseMerge(unsafe.Pointer(wg))
		}
		race.Disable()
		defer race.Enable()
	}
	state := atomic.AddUint64(statep, uint64(delta)<<32)	// 左移32位加到 counter上
	v := int32(state >> 32)	// counter
	w := uint32(state)			// waiter
	if race.Enabled && delta > 0 && v == int32(delta) {
		race.Read(unsafe.Pointer(semap))
	}
	if v < 0 {		// counter 是可正可负的,Done就是加了个 -1
		panic("sync: negative WaitGroup counter")
	}
  // Wait 先于 Add 被调用就会 panic,Add设置的值必须和实际等待的 goroutine个数相同
	if w != 0 && delta > 0 && v == int32(delta) {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
  // counter > 0 说明不需要释放信号量,waiter = 0 说明没有等待者,也不用释放信号量
	if v > 0 || w == 0 {
		return
	}
	if *statep != state {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
	// Reset waiters count to 0.
	*statep = 0
	for ; w != 0; w-- {
		runtime_Semrelease(semap, false, 0)  // 释放信号量,唤醒等待者
	}
}

Wait:

func (wg *WaitGroup) Wait() {
	statep, semap := wg.state()
	if race.Enabled {
		_ = *statep // trigger nil deref early
		race.Disable()
	}
	for {
		state := atomic.LoadUint64(statep)
		v := int32(state >> 32)		// counter
		w := uint32(state)				// waiter
		if v == 0 {			// counter = 0 说明所有的 goroutine都退出了,不用等待了
			if race.Enabled {
				race.Enable()
				race.Acquire(unsafe.Pointer(wg))
			}
			return
		}
    // 累加 waiter
		if atomic.CompareAndSwapUint64(statep, state, state+1) {
			if race.Enabled && w == 0 {
				race.Write(unsafe.Pointer(semap))
			}
			runtime_Semacquire(semap)		// 等待信号量唤醒
			if *statep != 0 {
				panic("sync: WaitGroup is reused before previous Wait has returned")
			}
			return
		}
	}
}

Done:

func (wg *WaitGroup) Done() {
	wg.Add(-1)
}

context:

对派生的子孙节点有很好的控制能力,呈现的是一种goroutine树状结构,每个goroutine 拥有相同的 ctx

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

实现Context接口的有四个

  • emptyCtx 这个就是我们熟知的 ctx.background
  • cancelCtx 可以cancel掉ctx派生的子cancel
  • timerCtx 在cancelCtx 基础上加了计时器,超时自动cancel
  • valueCtx 可以用于子协程读取到父协程key-value

可以互为父节点,根据特点可以组合多种形式

在这里插入图片描述

emptyCtx:

其他类型的Ctx 方法如果没有父Context,需要传入background作为父节点

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}
func (*emptyCtx) Done() <-chan struct{} {
	return nil
}
func (*emptyCtx) Err() error {
	return nil
}
func (*emptyCtx) Value(key any) any {
	return nil
}
func (e *emptyCtx) String() string {
	switch e {
	case background:
		return "context.Background"
	case todo:
		return "context.TODO"
	}
	return "unknown empty Context"
}

cancelCtx:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)		// 自身节点加到父节点上
	return &c, func() { c.cancel(true, Canceled) }
}

type cancelCtx struct {
	Context
	mu       sync.Mutex
	done     atomic.Value
	children map[canceler]struct{}	// 派生子协程
	err      error         
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
    // 关闭 chancel 并通知派生 Context
		close(d)
	}
  // 遍历 子Context 调用cancel
	for child := range c.children {
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()
  // 再将自己从父节点下移除
	if removeFromParent {
		removeChild(c.Context, c)
	}
}

timerCtx:

type timerCtx struct {
	cancelCtx
	timer *time.Timer 
	deadline time.Time
}

// 超时时间
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

// 截止时间
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) 
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

valueCtx:

type valueCtx struct {
	Context
	key, val any
}

func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
  // 当前节点找不到就会去父节点找,没有找到最终返回 interface{}
	return value(c.Context, key)
}

func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case *emptyCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

锁机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03WImqHf-1673361883713)(go%E4%B8%93%E5%AE%B6%E7%BC%96%E7%A8%8B.assets/76599410260b73898db7637b68d2e07c.jpg)]

type Mutex struct {
	state int32		// 锁状态
	sema  uint32	// 信号量
}
// 四种状态
const (
	mutexLocked = 1 << iota 	// 是否已被锁
	mutexWoken								// 是否有协程已被唤醒
	mutexStarving							// 是否处于饥饿状态
	mutexWaiterShift = iota		// 阻塞等待锁的协程个数
}

Normal模式

默认下,如果加锁不成功不会立即进入阻塞状态,而是先判断是否满足自旋条件,满足自旋条件就尝试抢锁

Starving模式

自旋能抢到锁,那么一定有协程释放了锁,有阻塞的协程被唤醒,但是唤醒后发现锁已被抢走只能继续阻塞,这次阻塞会与上次阻塞时间比较,超过1Ms则标记为饥饿,饥饿状态下不会自旋,下一次一定获得锁

Woken模式

同一时刻两个协程一个加锁一个解锁,加锁的在自旋检查,woken标记为1,通知解锁的协程不需要释放信号量,加锁的协程马上就拿到了

自旋

自旋很好理解,类似CPU空转sleep,时间很短,过程会持续检查Locked标记是否为0

自旋条件:自旋并不是无限制的,只有在不忙的时候才会自旋

  • 自旋次数足够少,最多4次
  • CPU多核
  • 协程调度中P的数量大于1
  • 协程可调度队列必须为空,否则延迟协程调度

自旋更充分利用CPU,尽量避免协程切换,短时间自旋可以获得锁就不需要进入阻塞

读写锁

type RWMutex struct {
	w           Mutex  
	writerSem   uint32 // 写阻塞信号量
	readerSem   uint32 // 读阻塞信号量
	readerCount int32  // 读者个数
	readerWait  int32  // 写阻塞时的读者个数
}
const rwmutexMaxReaders = 1 << 30

写操作如何阻止读操作:

代码大概是这个意思,readerCount - 2^30 就变成负数对吧,读锁来到时检查 readerCount < 0 就代表已经有了写锁,阻塞等待,要返回加回2^30 就行。

读操作如何阻止写操作:

读锁就简单了,readerCount +1,写锁来到时发现读者数量不等于0,阻塞等待所有读者退出

写锁定不会饿死吗:

写锁到来时要等待所有读者退出对吧,但是这个时候又有新的读者加入,readerCount +1 写锁会不会饿死呢,其实写锁到来时会把readerCount 复制给 readerWait 一份,读者退出他们两个都 -1,直到 readerWait = 0,也就说明写锁前的读者全部退出,readerCount中是写锁后的读者

反射

在这里插入图片描述

异常处理

errors包

errors包比较简单,简单提一下

  • New 自定义错误
  • Unwrap 获取原始的error
  • Is 检查error链中是否包含特定的error值
  • As 断言检查error是否是指定类型

defer

只要记住一个先进后出规则进行了,defer延迟函数会在return返回之前执行,return转成汇编其实就是RET

确定返回值 -> 执行defer -> RET跳转

type _defer struct {
	started bool
	heap    bool
	openDefer bool
	sp        uintptr // 函数栈指针
	pc        uintptr // 程序计数器
	fn        func()  // 调用函数
	_panic    *_panic // 
	link      *_defer // defer链
	fd   unsafe.Pointer 
	varp uintptr      
	framepc uintptr
}

type g struct {
	stack       stack   // 
	stackguard0 uintptr // 
	stackguard1 uintptr // 
	_panic    *_panic
	_defer    *_defer
  ............
}

在这里插入图片描述

panic

type _panic struct {
	argp      unsafe.Pointer 
	arg       any            
	link      *_panic      
	pc        uintptr       
	sp        unsafe.Pointer
	recovered bool           
	aborted   bool         
	goexit    bool
}

recover

func gorecover(argp uintptr) any {
	gp := getg()
	p := gp._panic
  // 触发panic时,recover必须被defer直接调用
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

定时器

Timer

单一事件的定时器,也就是说只执行一次就结束

// 新的Timer交给系统协程监控
func startTimer(*runtimeTimer)
// 系统协程删除指定的Timer
func stopTimer(*runtimeTimer) bool
// 系统协程删除指定Timer再添加一个新的
func resetTimer(*runtimeTimer, int64) bool

type Timer struct {
	C <-chan Time		//
	r runtimeTimer  // 定时器
}
// 会把一个定时任务交给专门的协程给到系统管理监控
type runtimeTimer struct {
	pp       uintptr	// 系统底层中定时器的存储数组地址
	when     int64		// 触发时间
	period   int64		// 周期触发间隔
	f        func(any, uintptr) 	// 触发时回调函数
	arg      any			// 回调函数的参数一
	seq      uintptr	// 回调函数的参数二
	nextwhen int64
	status   uint32
}

func NewTimer(d Duration) *Timer {
	c := make(chan Time, 1)
	t := &Timer{
		C: c,
		r: runtimeTimer{
			when: when(d),
			f:    sendTime,
			arg:  c,
		},
	}
	startTimer(&t.r)
	return t
}

// 向管道写入当前时间
func sendTime(c any, seq uintptr) {
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

// 自定义执行方法
func AfterFunc(d Duration, f func()) *Timer {
	t := &Timer{
		r: runtimeTimer{
			when: when(d),
			f:    goFunc,
			arg:  f,
		},
	}
	startTimer(&t.r)
	return t
}

在这里插入图片描述

Ticker

周期计时器,和Timer用法差不多,但有一点就是执行完必须有 Stop(),而Timer可以不用,执行结束会自动退出系统计时器监控,Ticker没有退出会一直占用CPU资源

type Ticker struct {
	C <-chan Time
	r runtimeTimer
}

func NewTicker(d Duration) *Ticker {
	if d <= 0 {
		panic(errors.New("non-positive interval for NewTicker"))
	}
	c := make(chan Time, 1)
	t := &Ticker{
		C: c,
		r: runtimeTimer{
			when:   when(d),
			period: int64(d),		// 执行周期
			f:      sendTime,
			arg:    c,
		},
	}
	startTimer(&t.r)
	return t
}

在这里插入图片描述

timersBucket

在Go1.14之前有 timer桶数组的概念,1.14以后优化掉了

type timersBucket struct {
	lock         mutex		
	gp           *g			// 处理堆中事件的协程
	created      bool   // 添加首个定时器后为true
	sleeping     bool		// gp 是否休眠
	rescheduling bool		// gp 是否已暂停
	sleepUntil   int64	// 事件处理协程睡眠事件
	waitnote     note		// 唤醒协程
	t            []*timer	// 定时器
}

在这里插入图片描述

Go在实现时预留了64个timer桶,当协程创建定时器时,使用协程所属的ProcessID%64来存入桶

在这里插入图片描述
1.14中取消桶,直接把保存timer的堆放到P中,P除了包含协程队列,还直接包含timer

在这里插入图片描述
1.14前定时器和其他协程一样也要竞争P调度

1.14后每次调度协程都会检查定时器需不需要处理,避免产生频繁的上下文切换

测试

单元测试

  • testing.T
  • 文件命名 xxx_test.go
  • 函数命名 TestXxx
  • 执行测试 go test

性能测试

  • testing.B
  • 函数命名 BenchmarkXxx
  • 执行测试 go test -bench= [Add/test] 正则匹配符合的性能函数
func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		AAA()
	}
}

func AAA() func(t testing.T) {
	return func(t testing.T) {
		t.Run("test1", add1)
		t.Run("test2", add2)
		t.Run("demo3", add3)
	}
}

在这里插入图片描述

示例测试

  • 不需要引testing包
  • 函数命名 ExampleXxx
  • 执行测试 go test

测试函数中没有output标识,代表测试函数没有被执行

子并发测试

  • go test -v Add.go Add_test.go 相关函数变量引用了的文件一起编译
  • 函数命名 TestXxx
  • 执行测试 go test -v Add.go Add_test.go -run Xxx[/test] 包含匹配,比如命名包含test的就会被执行

在这里插入图片描述
在这里插入图片描述
包含test的被执行,demo的没执行

Main测试

  • testing.M
  • 函数命名 TestMain
func TestMain(m *testing.M) {
	// 执行单元测试、性能测试、示例测试
	// 返回 0 全部通过,返回 1 有失败
	code := m.Run()
	os.Exit(code)
}
type M struct {
	deps        testDeps
	tests       []InternalTest       // 单元测试
	benchmarks  []InternalBenchmark  // 性能测试
	fuzzTargets []InternalFuzzTarget //
	examples    []InternalExample    // 示例测试
	timer       *time.Timer          // 超时时间
	afterOnce   sync.Once
	numRun      int
	exitCode    int
}

common

T 和 B 是在 common上做扩展

type common struct {
	mu          sync.RWMutex         // 读写锁
	output      []byte               // 测试日志
	w           io.Writer            // 子测试通过 w传递给父测试中
	ran         bool                 // 是否被执行过
	failed      bool                 // 当前测试失败,则 true
	skipped     bool                 // 是否已跳过当前测试
	done        bool                 // 当前测试及子测试已结束
	helperPCs   map[uintptr]struct{} //
	helperNames map[string]struct{}  //
	cleanups    []func()             //
	cleanupName string               //
	cleanupPc   []uintptr            //
	finished    bool                 // 当前测试结束,则 true
	inFuzzFn    bool                 //
	chatty      *chattyPrinter       // 对应 -v,打印更详细日志
	bench       bool                 //
	hasSub      int32                // 当前测试是否包含子测试
	raceErrors  int                  // 竞态检测错误数
	runner      string               // 当前测试函数名
	parent      *common              // 父测试指针
	level       int                  // 测试嵌套层数
	creator     []uintptr            // 测试函数调用栈
	name        string               // 记录每个测试函数名
	start       time.Time            // 记录测试开始时间
	duration    time.Duration        // 记录花费时间
	barrier     chan bool            // 控制父测试和子测试执行的channel
	signal      chan bool            // 通知当前测试结束
	sub         []*T                 // 子测试列表
	tempDirMu   sync.Mutex           //
	tempDir     string               //
	tempDirErr  error                //
	tempDirSeq  int32                //
}

TB、T、B

type TB interface {
	Cleanup(func())
	Error(args ...any)
	Errorf(format string, args ...any)
	Fail()
	FailNow()
	Failed() bool
	Fatal(args ...any)
	Fatalf(format string, args ...any)
	Helper()
	Log(args ...any)
	Logf(format string, args ...any)
	Name() string
	Setenv(key, value string)
	Skip(args ...any)
	SkipNow()
	Skipf(format string, args ...any)
	Skipped() bool
	TempDir() string
	
  // 私有接口用于控制接口唯一性
  // 即使用户代码实现了这些方法,但是无法实现这个私有接口,也就无法实现 TB接口
	private()
}

type T struct {
	common
	isParallel bool		// 当前测试是否需要并发 
	isEnvSet   bool		
	context    *testContext		// 控制测试并发调度
}

type B struct {
	common
	importPath       string
	context          *benchContext
	N                int // 目标代码执行次数,会自行调整
	previousN        int
	previousDuration time.Duration
	benchFunc        func(b *B)          // 性能测试函数
	benchTime        durationOrCountFlag // 测试函数最少执行时间,默认1s
	bytes            int64               // 每次迭代处理的字节数
	missingBytes     bool
	timerOn          bool // 是否开始计时
	showAllocResult  bool
	result           BenchmarkResult // 测试结果
	parallelism      int
	startAllocs      uint64             // 开始时堆中分配的对象总数
	startBytes       uint64             // 开始时堆中分配的字节总数
	netAllocs        uint64             // 结束时时堆中分配的对象总数
	netBytes         uint64             // 结束时堆中分配的对象总数
	extra            map[string]float64 //
}
  • -timeout 超时时间
  • -args 参数列表
  • -json 测试结果转换成JSON格式
  • -o 生成测试二进制程序
  • -benchtime s 每个性能测试时间,默认1s
  • -count n 每个测试执行次数,默认1次
  • -parallel n 设置最大并发数
  • -timeout xs|xm|xh 超时时间,默认超过10分钟退出
  • -benchmem 性能测试同时打印每个操作分配的字节数和对象数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/153727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【NI Multisim 14.0原理图设计基础——查找元器件】

目录 序言 一、查找元器件 &#x1f34d;1.浏览元器件 &#x1f34d;2.搜索元器件 &#x1f34d; 3.显示找到的元器件及其所属元器件库 &#x1f34d; 4.加载找到元器件的所属元器件库 序言 NI Multisim最突出的特点之一就是用户界面友好。它可以使电路设计者方便、快捷地…

大资金现金管理的利器:稳定币网格做市策略

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

从一条记录说起—— InnoDB 记录结构

准备工作 到现在为止&#xff0c;MySQL对于我们来说还是一个黑盒&#xff0c;我们只负责使用客户端发送请求并等待服务器返回结果&#xff0c;表中的数据到底存到了哪里&#xff1f;以什么格式存放的&#xff1f;MySQL是以什么方式来访问的这些数据&#xff1f;这些问题我们统…

springboot构造树形结构数据并查询的方法

因为项目需要,页面上需要树形结构的数据进行展示(类似下图这样),因此需要后端返回相应格式的数据。 不说废话,直接开干!!! 我这里用的是springboot+mybatis-plus+mysql,示例的接口是查询一级权限以及二级权限、三级权限整个权限树… 下面是导入的maven依赖 <depe…

SSD核心设计

摘要本文介绍了此类设计选择的分类&#xff0c;并使用跟踪驱动的模拟器和从实际系统中提取的工作负载跟踪分析各种配置的可能性能。我们发现SSD性能和生命周期对工作负载非常敏感&#xff0c;并且通常较高的复杂系统问题出现在存储堆栈中甚至在分布式系统中&#xff0c;与设备固…

9. 回文数

文章目录题目描述方法一 转换为字符串方法二 转存入数组方法三 数学方法倒转数字方法四 对折参考文献题目描述 给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 回文数是指正序&#xff08;从左向右&…

树脂吸附处理冶炼含钴丨废水四氯化三钴、草酸钴废水回收钴

生产过程中会产生含钴废水&#xff0c;现有技术中&#xff0c;处理含钴废水的方法主要有化学沉淀法、吸附法、膜分离法等。 对于化学沉淀法&#xff0c;原理是通过向废水中添加一定的沉淀剂&#xff0c;使其与废水中溶解性的钴离子反应生成沉淀&#xff0c;通过重力沉降去除钴…

ArcGIS 前端动态地图与要素服务符号化的区别小结

在ArcGIS中&#xff0c; 除了在桌面端做配图之外&#xff0c;一些符号化工作也可以在代码端去进行设置&#xff0c;这里简单的做了一些测试及小结。 一、服务的区别 在ArcGIS中最基础的两种数据服务就是动态地图服务&#xff08;MapServer&#xff09;和要素服务&#xff08;…

Spring Boot 教程

Spring Boot 教程Spring Boot 基础开发Spring Boot 简介Spring Boot 第一个项目Spring Boot 项目启动机制Spring Boot 数据访问Spring Boot 集成 MyBatisSpring Boot 运行管理Spring Boot 日志管理Spring Boot 异常处理Spring Boot 定时任务Spring Boot 使用拦截器Spring Boot …

C#汽车美容管理服务系统源码 功能强大代码完整,开源分享!

一套完整的汽车美容管理服务系统源码&#xff0c;专门服务于汽车美容4s店&#xff0c;终端功能强大而又简便实用&#xff0c;界面友好而美观&#xff0c;让用户更好的体验度,基于jquery技术实现页面无刷新,可广泛适用于大型以及小型汽车美容机修等公司&#xff0c;包含 洗车、机…

【异常】记一次前端因资源无法加载导致白屏异常问题

一、背景 自从运维同事强烈要求前端的环境要使用多套的&#xff0c;参考文章 【项目】参考若依的前端框架去多环境 于是一番捣鼓与改造之后&#xff0c;看似已经顺利了 但运维说&#xff0c;前端还是有问题&#xff0c;需要他帮我改下&#xff0c;于是改了如下内容 什么嘛&a…

常用的git管理逻辑

//首先创建新的分支 git branch new_branch//切换到另一个分支 git checkout new_branch//修改代码&#xff0c;添加新的文件 git add .//查看文件的状态 git status//打包自己的改变 git commit -m "comment"//推到远程服务器上 git push origin new_branch// 拉远程…

2022年度打印机行业数据报告:十大热门品牌销量排行榜

随着5G技术的发展&#xff0c;越来越多的设备与人员将通过网络实现互联互通&#xff0c;打印的输入端口与输出端口变得多样与兼容&#xff0c;打印机也在越来越多的领域发挥着强大的作用。并且&#xff0c;随着电子化教学的不断推进以及普及&#xff0c;加之线上办公等场景的需…

Hudi系列2:Hudi安装

文章目录编译环境准备一. 下载并解压hudi二. maven的下载和配置2.1 maven的下载和解压2.2 添加环境变量到/etc/profile中2.3 修改为阿里镜像三. 编译hudi3.1 修改pom文件3.2 修改源码兼容hadoop33.3 手动安装Kafka依赖3.4 解决spark模块依赖冲突3.4.1 修改hudi-spark-bundle的p…

LeetCode 3. 无重复字符的最长子串

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 3. 无重复字符的最长子串&#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 目录 一、题目名称…

(02)Cartographer源码无死角解析-(48) 2D点云扫描匹配→暴力搜索匹配原理讲解,扫面匹配总体框架梳理

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文末…

2022消费市场并未降级?麦肯锡调研报告解读

近日全球知名咨询公司麦肯锡发布《2023中国消费者报告》&#xff0c;重磅发布重塑中国消费市场的五大趋势。 趋势一&#xff1a;中等收入人群继续壮大 MGI数据显示&#xff0c;2019~2021年&#xff0c;年收入超过16万元人民币&#xff08;2.18万美元&#xff09;的中国城镇家…

nacos配置动态刷新及监听到变化触发一些方法

介绍在使用spring 时&#xff0c;动态更新配置是常见的&#xff0c;属性值更新&#xff0c;但是需要开启支持刷新功能&#xff0c;一个是spring.cloud.nacos.config.isRefreshEnabledtrue; 这个值一般是默认的&#xff0c;可以在nacosConfigProperties这个类中看到。还要在扩展…

【C++】list介绍及使用模拟实现对比vector

文章目录1. list的介绍及使用1.1 list的介绍1.2 list的使用1.2.1 list的构造1.2.2 list iterator以及insert和erase的使用1.2.3 list的迭代器失效2. list的模拟实现2.1 模拟实现list3.&#xff08;本文精华&#xff09;list与vector的对比1. list的介绍及使用 1.1 list的介绍 …

图像识别AI程序(数据集管理/训练/预测)

出于对Python中AI智能识别程序的兴趣&#xff0c;对照AI智能识别程序应该具有的主要功能&#xff1a;数据集管理&#xff08;加载/移除/重命名&#xff09;、模型训练&#xff08;可实时监测训练进度、最终训练参数输出&#xff09;、模型预测&#xff0c;使用Python/Keras编制…