Go第三方框架--ants协程池框架

1. 背景介绍

1.1 goroutine

ants是站在巨人的肩膀上开发出来的,这个巨人是goroutine,这是连小学生都知道的事儿,那么为什么不继续使用goroutine(以下简称go协程)呢。这是个思考题,希望讲完本文大家可以有个答案。
go协程只涉及用户态的使用,不涉及内核态和两态的切换,所以非常轻便,通常一个协程大概只占用2k的内存,比线程更轻量级,而且其还有特别高效的GMP协程调度算法,使得go语言编写并发程序简单和高效。但是官方没有提供协程池包,虽然go协程有如此多的优点,go语言也支持垃圾自动回收,但是不断地对资源进行创建和回收是一种犯罪行为;为了更好的支持协程资源空间重复使用、并发控制、提升性能,并为开发者提供更简便和使用的功能,有大量的第三方协程池框架应运而生,ants是其中的佼佼者。

2. ants简介

ants是github托管的一个高效的协程池库,其star已经有12k了,足见其受欢迎的程度。本ants版本使用的是v2.8.2 目前最新版是2.9.1。其主要committer是潘建锋,根据公开资料(应该没侵犯隐私)曾任职腾讯,现在在亚马逊上班,未婚,个人博客。八卦完了,现在看看ants的发展史吧。ps:为了还原历史,下文引用ants的readme 相信很多博客都介绍过了,纯属凑字数。

2.1 简介

ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。

2.2 功能
  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
  • 非阻塞机制
2.3 ants是如何运行的

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

2.4 使用
// demoPoolFunc1 执行任务
func demoPoolFunc1() {
	time.Sleep(2 * time.Millisecond)
}

// TestAntsPoolWaitToGetWorker ants运行简单例子.
func TestAntsPoolWaitToGetWorker(t *testing.T) {
	var wg sync.WaitGroup
	p, _ := NewPool(1000) // 初始化一个协程池 容量是 1000
	defer p.Release()     // 执行成功手动释放协程池

	for i := 0; i < 10000; i++ { // 启动10000个任务 让协程池来运行
		wg.Add(1)             // wg++
		_ = p.Submit(func() { // 提交任务
			demoPoolFunc1()
			wg.Done() // 完成后 wg--
		})
	}
	wg.Wait() // 阻塞等待任务完成
}

可以看到 暴露给外面的 就是 图2.3的 pool池初始化、任务提交和pool池回收 剩下的 都在水面之下 . ps:submit 也可以使用协程启动 这样 下方代码中出现的锁 就可以解释的通了。
以下 源码讲解就拿上述代码为例子

ps: 还有一些内容 请看客移步代码的readme

3. 几种重要的结构体

3.1 pool

pool核心结构体,其结构体 如下

// Pool 接受来自客户端的任务,通过循环利用 goroutines 限制了总数量为给定值。
type Pool struct {
   // pool 的容量,负值表示 pool 容量无限,使用无限 pool 是为了避免由于 pool 的嵌套使用(向同一个 pool 提交任务,该任务又向同一个 pool 提交新任务)可能引起的无限阻塞问题。
   capacity int32
   
   // 当前运行的 goroutines(goworker的items长度) 数量。
   running int32
   
   // 保护 worker 队列的锁。
   lock sync.Locker

   // 存储可用 worker 的切片,是任务执行器的队列,是一个接口 
   workers workerQueue

   // 状态用于通知 pool 关闭自身。
   state int32

   // 等待获取空闲 worker 的条件。
   cond *sync.Cond

   // workerCache  池化技术 不断生成 新worker , 但是他不是 2.3工作池。它在woker数量没达到容量阈值时,
   // 会不断生成。一旦达到了,就不会再工作,因为这时 workerStack 中的 items 满了后会自己维持(因为一般不会有过期,又有容量限制),后续会讲解。
   // 直到将过期的worker放入队列时,woker数量减少,才又继续工作生成新的worker。但是 一般情况下 过期时间都特别长,当过期时间短时,
   // 工作池就是 workers + workerCache  这两个属性 在不断向items补充 gowoker。workers回收可用的goworker,workerCache 补齐不足容量的部分。
   // 但是一般不会让goworker过期,所以为了方便 我们一般吧 items叫做工作池
   workerCache sync.Pool

   // waiting 是已经在 pool.Submit() 上被阻塞的 goroutines 数量(因为本2.3例子没有使用协程提交submit,所以最大为1),受 pool.lock 保护。
   waiting int32

   purgeDone int32
   stopPurge context.CancelFunc // cancel 用来停止 goPurge 函数
   ticktockDone int32
   stopTicktock context.CancelFunc // cancel 用来停止 goTicktock 函数
   now atomic.Value  // 存储现在时间
	
	// 存放一些参数 过期时间 最大阻塞任务等
   options *Options

}

3.2 workerStack

任务的执行队列

type workerStack struct {
	items  []worker // 工作池, 对应 2.3 图中的 工作池 每个工作池 运行一个 协程 
	expiry []worker // 已经过期的任务执行器队列(工作池)
}

这个结构体实现了 workerQueue 结构体的接口 也就是 3.1pool里的 workerQueue 。

type workerQueue interface {
	len() int
	isEmpty() bool
	insert(worker) error    // 向工作池中 加入一个 任务执行器
	detach() worker   // 从工作池中 拿到一个 任务执行器
	refresh(duration time.Duration) []worker //  清理掉过期的goworker
	reset()  // 重置 将所有goworker 任务执行完毕后 结束
}

接口中都是操作工作执行器(goworker)队列的一些函数

3.3 goWorker

goWorker 是实际任务执行器

// goWorker 是实际执行任务的执行器,
// 它启动一个 goroutine 来接受任务并执行函数调用。
type goWorker struct {
	// 拥有该 worker 的池 也就是 3.1中的 pool, 将pool传给 worker 以便调用pool的函数和属性。
	pool *Pool

	// task 存放 执行任务的chan。 将任务输送到chan来执行。
	task chan func()

	// lastUsed 在将 worker 放回队列时将更新,有多个任务则时间是最后一个任务执行时间。
	lastUsed time.Time
}

其实现了下面的 worker 结构体


type worker interface {
	run()   // 运行 goworker
	finish() // 让任务执行完毕后 结束goworker 
	lastUsedTime() time.Time
	inputFunc(func()) // 向 goworker 中的 chan 传递任务
	inputParam(interface{})
}

接口中都是操作工作执行器的一些函数

ps: 之所以需要执行器和其队列的接口 是因为有不同的执行器和执行器队列,例如goWorkerWithFunc执行器和 loopQueue执行器队列,因为不是本文重点不做介绍,但其大体执行逻辑是一致的,感兴趣的看官可以自行研究。

如上我们就可以猜到执行的一个大体的脉络,控制逻辑是 pool–>workerStack(2.3中的工作池,主要是属性items)–>goworker(worker 任务执行器),接下来我们来看下源码 验证下

4. ants 协程池初始化

见 2.4 代码部分 p, _ := NewPool(1000) ,这样可以初始化一个pool池。我们看下其源码,

// NewPool generates an instance of ants pool.
func NewPool(size int, options ...Option) (*Pool, error) {
	if size <= 0 {
		size = -1
	}

	opts := loadOptions(options...) // opts 没有传 跳过

	if !opts.DisablePurge { // 值是 true 走这里
		if expiry := opts.ExpiryDuration; expiry < 0 {
			return nil, ErrInvalidPoolExpiry
		} else if expiry == 0 {
			opts.ExpiryDuration = DefaultCleanIntervalTime // 过期时间
		}
	}

	if opts.Logger == nil { // logger 选择默认的
		opts.Logger = defaultLogger
	}

	p := &Pool{ // 初始化 pool 将size赋值给capacity
		capacity: int32(size),
		lock:     syncx.NewSpinLock(), // 这里是自己实现的指数退避的自旋锁,为啥要自己实现我想可能是官方的性能作者不满意吧 有空细研究下
		options:  opts,
	}
	p.workerCache.New = func() interface{} { //  从缓存池中可以获取新的 goworker
		return &goWorker{
			pool: p,
			task: make(chan func(), workerChanCap), // 创建一个 大小是1的任务chan,注意大小是0和1不一样,感兴趣的自行gpt3.5
		}
	}
	if p.options.PreAlloc { // false 不走这里
		if size == -1 {
			return nil, ErrInvalidPreAllocSize
		}
		p.workers = newWorkerQueue(queueTypeLoopQueue, size)
	} else {
		p.workers = newWorkerQueue(queueTypeStack, 0) // 初始化 workerStack(工作池) 结构体 大小是0
	}

	p.cond = sync.NewCond(p.lock)

	p.goPurge()    // 启动一个协程 不断检查执行器队列,将过期的执行器(goworker)的任务释放,任务对应的任务执行器放入 pool中,工作执行器协程退出;
	p.goTicktock() // 启动协程更新pool的时间,用来更新 goworker的最后使用时间

	return p, nil
}

到这里 一个新的 pool就建立起来了 ,这其中需要进一步剖析的是 p.goPurge() 其 值得关注的调用链 如下

在这里插入图片描述

purgeStaleWorkers(ctx context.Context) 函数代码如下

// 启动一个协程 不断检查执行器队列,将过期的执行器的任务释放,任务对应的任务执行器放入 workercache 中,工作执行器协程退出;
func (p *Pool) purgeStaleWorkers(ctx context.Context) {
	ticker := time.NewTicker(p.options.ExpiryDuration) // 使用过期时间 建一个 ticker

	defer func() {
		ticker.Stop()
		atomic.StoreInt32(&p.purgeDone, 1)
	}()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C: // 到了过期时间 就开始执行 查找过期woker的逻辑
		}

		if p.IsClosed() {
			break
		}

		var isDormant bool
		p.lock.Lock()                                               //  为什么加锁  (因为submit可以使用 协程启动) 
		staleWorkers := p.workers.refresh(p.options.ExpiryDuration) // 将 items里面 过期的任务队列 复制到 expires队列里 并 返回
		n := p.Running()
		isDormant = n == 0 || n == len(staleWorkers)
		p.lock.Unlock()

		// Notify obsolete workers to stop.
		// This notification must be outside the p.lock, since w.task
		// may be blocking and may consume a lot of time if many workers
		// are located on non-local CPUs.
		for i := range staleWorkers { // 将过期队列 依次调用 finish() 使得本 woker停止(因为本worker的任务执行完毕后 会 阻塞,当调用finish()时,会给chan传递一个 nil 使得任务停止 详细 请看 worker.run()) 并加入workercache 中
			staleWorkers[i].finish()
			staleWorkers[i] = nil
		}

		// There might be a situation where all workers have been cleaned up (no worker is running),
		// while some invokers still are stuck in p.cond.Wait(), then we need to awake those invokers.
		if isDormant && p.Waiting() > 0 { // 如果 不存在运行的协程(调用 run()会启动一个协程 数量 跟执行器相等 可以看做一个协程就是一个运行着的执行器 也就是 len(items)),或者 等待的协程>1 就广播 通知等待的协程来运行(主要是来提交任务)
			p.cond.Broadcast()
		}
	}
}

到这里 goPurge()就讲解 完毕了 。

5. 任务提交

任务 提交 到 任务 处理的 调用链 如下
在这里插入图片描述

run 函数是具体执行 任务 的函数,它启动一个协程来处理submit函数提交的任务
池化完毕后 就是 任务提交 p.Submit(…),这是核心执行逻辑 见 2.3 中 提交后 后续的执行逻辑都在这个函数里。 老规矩,我们继续追踪源码

5.1 任务提交
func (p *Pool) Submit(task func()) error {  // 其接受一个 任意类型的 任务 
	if p.IsClosed() {      // 如果协程池关闭了 则报错
		return ErrPoolClosed
	}

	w, err := p.retrieveWorker()  // 获取一个工作执行器(用 goworker 简称),规则:大体逻辑 如果工作池有,就拿来用,如果没有就新建(还有阻塞等)见 2.3流程图
	if w != nil {
		w.inputFunc(task)  // 将 任务 加入到 获取的 goworker 的任务chan 中(这时 goworker的run()函数可能正在 chan处阻塞),开始执行任务。大家思考下这边会不会阻塞?? 
	}
	return err
}

其中 retrieveWorker() (w worker, err error) 函数如下

5.2 任务获取
// 这个应该是核心执行逻辑,返回一个可以使用的w或者阻塞
// retrieveWorker returns an available worker to run the tasks.
func (p *Pool) retrieveWorker() (w worker, err error) {
	p.lock.Lock() 

retry:
	// First try to fetch the worker from the queue.
	if w = p.workers.detach(); w != nil { //  items(工作池) 头部goworker出栈 赋值给 w,如果有空闲的w(w!=nil) 则返回
		p.lock.Unlock()
		//  为什么得到 w 后不run() 而是直接返回   ps: 因为只要可以获取到 工作执行器 则 必定 有一个 协程正在运行,否则这个执行器就是过期的退出了 不在 items中。
		return
	}

	// If the worker queue is empty and we don't run out of the pool capacity,
	// then just spawn a new worker goroutine.
	// 如果工作队列是空的或者 正在跑的 协程数(items大小)没达到 容量,任务刚启动时,会走这边,一旦items满了后,基本不走这边 要么从 items直接获取任务(上面代码),要么阻塞(下方代码 p.addWaiting)
	if capacity := p.Cap(); capacity == -1 || capacity > p.Running() {
		p.lock.Unlock()
		w = p.workerCache.Get().(*goWorker) // 从pool缓冲池中获取一个 工作队列,注意 workerCache 不是工作池,而是 对应2.3 “新启动一个 woker 来执行任务 ”的前半句话。
		w.run()  // 这边 开始 启动协程 处理 任务。思考题:items数量和协程数量是否是一致的(一致)
		return
	}

	// Bail out early if it's in nonblocking mode or the number of pending callers reaches the maximum limit value.
	if p.options.Nonblocking || (p.options.MaxBlockingTasks != 0 && p.Waiting() >= p.options.MaxBlockingTasks) { 
		p.lock.Unlock()
		return nil, ErrPoolOverload
	}

	// Otherwise, we'll have to keep them blocked and wait for at least one worker to be put back into pool.
	p.addWaiting(1) //能走到这里,证明items 达到最大容量了。
	p.cond.Wait()   // block and wait for an available worker// 程序阻塞等待唤醒,任务执行完毕或者goworker过期都会唤醒。
	p.addWaiting(-1)

	if p.IsClosed() {
		p.lock.Unlock()
		return nil, ErrPoolClosed
	}

	goto retry  // 被唤醒后 走到 retry 重新开始 为任务选 gowoker
}

在上述代码 w = p.workerCache.Get().(*goWorker) 处,我们可以发现 ,每次从缓存池获取一个 goworker 都要调用 run()函数,来启动goworker,这个函数才是执行任务的核心代码。它会启动一个协程 并采用chan阻塞等待任务得到来。下面介绍run()函数

5.3 任务执行

这里开始执行任务

func (w *goWorker) run() {
	w.pool.addRunning(1) // 运行的 goworker 数量+1
	go func() {
		defer func() { // 当 goworker过期 或者 批量任务执行完毕 调用 p.Release(), 下方 for 循环 退出,然后则调用 本defer
			w.pool.addRunning(-1)     // 正在运行的 goworker 数量-1
			w.pool.workerCache.Put(w) // 将不用的 goworker 放入 缓存池中
			if p := recover(); p != nil {
				if ph := w.pool.options.PanicHandler; ph != nil {
					ph(p)
				} else {
					w.pool.options.Logger.Printf("worker exits from panic: %v\n%s\n", p, debug.Stack())
				}
			}
			// Call Signal() here in case there are goroutines waiting for available workers.
			w.pool.cond.Signal() // 唤醒一个 在 retrieveWorker()函数 中 代码	p.cond.Wait()  处阻塞的程序,开始提交任务
		}()

		for f := range w.task { // 从 5.1submit()函数的 inputFunc(func())调用 处获取任务

			if f == nil { // 如果 chan 传递的是 nil (调用 finish()),则这个goworker退出
				return
			}
			f()                                    // 开始执行任务 执行2.4中例子中的 demoPoolFunc1()
			if ok := w.pool.revertWorker(w); !ok { // 任务执行完毕,将本goworker 入栈 items ,这时 协程不退出 继续for循环,这边就实现了 重复利用。
				return
			}
		}
	}()
}

任务执行完成后,对应的goworker需要入栈 在代码 w.pool.revertWorker(w) 处执行,下面介绍下这个函数

5.4 工作执行器(goworker)入栈

本小结主要是将闲下来的goworker入栈 涉及到 revertWorker(worker *goWorker) 函数,其代码如下:

// revertWorker puts a worker back into free pool, recycling the goroutines.
// 直接翻译: 将goworker放入 items中,循环使用这个协程
func (p *Pool) revertWorker(worker *goWorker) bool {
	if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() { 
		p.cond.Broadcast()
		return false
	}

	worker.lastUsed = p.nowTime() //  更新goWorker协程的最后使用时间

	p.lock.Lock()
	// To avoid memory leaks, add a double check in the lock scope.
	// Issue: https://github.com/panjf2000/ants/issues/113
	if p.IsClosed() {
		p.lock.Unlock()
		return false
	}
	if err := p.workers.insert(worker); err != nil { // 将 goWorker 入栈
		p.lock.Unlock()
		return false
	}
	// Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue.
	p.cond.Signal() /// 唤醒一个 在 retrieveWorker()函数 中 代码	p.cond.Wait()  处阻塞的程序(如果是协程启动的submit,则将此协程唤醒)
	p.lock.Unlock()

	return true
}

到此 我们2.3的流程图涉及到的所有模块都梳理完毕,接下来就等着任务执行完毕后,将协程池释放。

6. 协程池释放

释放所有跟pool相关的资源,我们来梳理下 有几处在运行或阻塞的程序
a. 提交任务时,当多于items容量时,阻塞在 5.2 函数的 p.cond.Wait() 处(要是submit是协程提交的话这里阻塞更多,,现在最多阻塞1个)
b. 新建 池时 启动的两个协程 见 标题4
c. submit提交任务时,启动的 items里的协程
所以有四处需要关闭,我们看下源码

func (p *Pool) Release() {
	if !atomic.CompareAndSwapInt32(&p.state, OPENED, CLOSED) { // 使用 cas算法 将 pool状态 修改为关闭,为续资源关闭做准备。
	    // 则所有协程任务执行完毕后,入 items 队列时会退出见 revertWorker() 函数 第一行)
	 	// 所有阻塞的协程 会退出(见 5.2 retrieveWorker()函数 最后部分 p.IsClosed()),其他见 对 p.IsClosed() 的调用
		return
	}

	if p.stopPurge != nil { // 停止 goPurge()函数 对应 --> b
		p.stopPurge()
		p.stopPurge = nil
	}
	p.stopTicktock() //  对应 --> b
	p.stopTicktock = nil

	p.lock.Lock()
	p.workers.reset() // 将所有goworker停止 对应 --> c
	p.lock.Unlock()
	// There might be some callers waiting in retrieveWorker(), so we need to wake them up to prevent
	// those callers blocking infinitely.
	p.cond.Broadcast() // 广播给 在 p.cond.wait()处阻塞的协程 继续运行,后续调用 p.IsClosed()退出。 这里对应   --> a

到这里ants的真个生命流程就梳理完毕了。

7.总结

感觉还是得梳理源码,才能有长进。这次只是梳理了最常用的核心部分,还有其他一些功能或结构体没有梳理到,希望有时间再添加吧。由于本人水平有限,本次梳理难免有疏漏,还请各位大佬指正,互相学习,谢谢。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python中使用print方法打印时显示颜色

使用说明 在编程中&#xff0c;使用颜色来区分不同类型的输出或突出显示关键信息是一种常见的做法&#xff0c;特别是在调试和日志记录过程中。以下是一些使用颜色输出的常见场景和用途&#xff1a; 调试信息&#xff1a;在调试代码时&#xff0c;可以使用不同颜色来区分不同级…

CSDN 通过博客关注了你

CSDN 通过博客关注了你 前言通过“博文XXX”和“博客”的区别 前言 最近新增粉丝里发现&#xff0c;粉丝来源有不同的方式&#xff1a; 通过博文XXX关注了你 通过你的主页关注了你 通过用户推荐关注了你 通过博客关注了你 通过“博文XXX”和“博客”的区别 通过博文XXX和…

序列化、反序列化:将对象以字节流的方式,进行写入或读取

序列化&#xff1a;将指定对象&#xff0c;以"字节流"的方式写入一个文件或网络中。 反序列化&#xff1a;从一个文件或网络中&#xff0c;以"字节流"的方式读取到对象。 package com.ztt.Demo01;import java.io.FileNotFoundException; import java.io.Fi…

LeetCode-2009. 使数组连续的最少操作数【数组 哈希表 二分查找 滑动窗口】

LeetCode-2009. 使数组连续的最少操作数【数组 哈希表 二分查找 滑动窗口】 题目描述&#xff1a;解题思路一&#xff1a;正难则反滑动窗口解题思路二&#xff1a;0解题思路三&#xff1a;0 题目描述&#xff1a; 给你一个整数数组 nums 。每一次操作中&#xff0c;你可以将 n…

rabbitmq延迟队列的使用

rabbitmq延迟队列的使用 1、场景&#xff1a; 1.定时发布文章 2.秒杀之后&#xff0c;给30分钟时间进行支付&#xff0c;如果30分钟后&#xff0c;没有支付&#xff0c;订单取消。 3.预约餐厅&#xff0c;提前半个小时发短信通知用户。 A -> 13:00 17:00 16:30 延迟时间&a…

麒麟V10安装Redis6.2.6

1、下载redis安装包 Redis各版本下载&#xff1a;https://download.redis.io/releases/ 2、将下载后的.tar.gz压缩包上传到到服务器自定义文件夹下 3、 解压文件 tar -zxvf redis-6.2.6.tar.gzmv redis-6.2.6 redis4、安装redis 在redis文件夹下输入make指令 cd /opt/redi…

性能测试 —— Jmeter 命令行详细

我们在启动Jmeter时 会看见&#xff1a;Don’t use GUI mode for load testing !, only for Test creation and Test debugging.For load testing, use CLI Mode (was NON GUI) 这句话的意思就是说&#xff0c;不要使用gui模式进行负载测试&#xff0c;gui模式仅仅是创建脚本…

【LeetCode: 628. 三个数的最大乘积 + 排序 + 贪心】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

C++ linked_hash_map按顺序保存的容器

HashMap中不存在保存顺序的机制。而在LinkedHashMap中可以保持两种顺序&#xff0c;分别是插入顺序和访问顺序&#xff0c;这个是可以在LinkedHashMap的初始化方法中进行指定的。相对于访问顺序&#xff0c;按照插入顺序进行编排被使用到的场景更多一些&#xff0c;所以默认是按…

实现鼠标在页面点击出现焦点及大十字星

近段时间&#xff0c;在完成项目进度情况显示时候&#xff0c;用户在操作鼠标时候&#xff0c;显示当鼠标所在位置对应时间如下图所示 代码实现步骤如下&#xff1a; 1.首先引用 jquery.1.7.js 2.再次引用raphael.js 3.然后引用graphics.js 4.最后引用mfocus.js 其中mfocu…

【leetcode面试经典150题】38. 生命游戏(C++)

【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主&#xff0c;题解使用C语言。&#xff08;若有使用其他语言的同学也可了解题解思路&#xff0c;本质上语法内容一致&…

蓝桥杯第九届省赛真题代码——彩灯控制器-附详细讲解思路

1. 比赛题目要求 2. 功能实现推荐步骤 首先&#xff0c;添加头文件&#xff0c;搭建最底层的代码&#xff0c;实现基本的流水灯运转与数码管显示rb2的电阻值 然后&#xff0c;进行pwm脉宽调制&#xff0c;实现rb2数值不同&#xff0c;从而灯光亮度不同。并作出数码管的多窗口…

Java GC了解

Jstack找到线程的快照 jvm提供其他命令作用 jps&#xff1a; 虚拟机进程状况工具&#xff0c;类似linux的ps命令 jstat&#xff1a;虚拟机统计信息监视工具&#xff0c;经常看gc情况的会使用到 jinfo: java配置信息工具 jmap&#xff1a; java内存映射工具&#xff0c;dump&am…

别催了!超真实格行5G随身WiFi问答它来了!格行5G随身WiFi靠谱吗? 看完这篇文章你就懂了?

总让我测格行5G随身WiFi&#xff0c;一直催催催。这下别催了&#xff0c;你们要的格行5G随身WiFi真实测评它来了&#xff01;这次着重回答大家最关心&#xff0c;问的最多的几个问题&#xff01; 一、问&#xff1a;格行5G随身WiFi网速怎么样&#xff1f; 答&#xff1a;格行5G…

网络编程套接字(一)

目录 一、源IP和目的IP 二、端口号 三、UDP协议和TCP协议 四、网络字节序 五、socket编程 1、socket 常见接口 2、struct sockaddr结构体 一、源IP和目的IP IP地址是IP协议提供的一种统一的地址格式&#xff0c;它为互联网上的每一个网络和每一台主机分配一个逻辑地址&am…

原子操作和竞争条件

所有系统调用都是以原子操作方式执行的。之所以这么说&#xff0c;是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行&#xff0c;其间不会为其他进程或线程所中断。原子性是某些操作得以圆满成功的关键所在。特别是它规避了竞争状态&#xff08;race condi…

解决ModuleNotFoundError: No module named ‘exceptions‘

一、问题描述 使用python语言处理docx文档&#xff0c;在安装docx库时出现问题&#xff0c;No module named ‘exceptions‘ 二、解决方法 卸载docx&#xff0c;安装python-docx。 pip uninstall docx pip install python-docx 问题解决&#xff01;

SSRF靶场

SSRF概述 ​ 强制服务器发送一个攻击者的请求 ​ 互联网上的很多web应用提供了从其他服务器&#xff08;也可以是本地)获取数据的功能。使用用户指定的URL&#xff0c;web应用可以获取图片&#xff08;载入图片&#xff09;、文件资源&#xff08;下载或读取)。如下图所示&…

[lesson17]对象的构造(上)

对象的构造(上) 对象的初始化 从程序设计的角度&#xff0c;对象只是变量&#xff0c;因此&#xff1a; 在栈上常见对象时&#xff0c;成员变量初始为随机值在堆上创建对象时&#xff0c;成员变量初始为随机值在静态存储区创建对象时&#xff0c;成员变量初始为0值 生活中的对…

算法打卡day41|动态规划篇09| Leetcode198.打家劫舍、213.打家劫舍II、337.打家劫舍 III

算法题 Leetcode 198.打家劫舍 题目链接:198.打家劫舍 大佬视频讲解&#xff1a;198.打家劫舍视频讲解 个人思路 偷还是偷&#xff0c;这取决于前一个和前两个房是否被偷了&#xff0c;这种存在依赖关系的题目可以用动态规划解决。 解法 动态规划 动规五部曲&#xff1a;…