「实验记录」MIT 6.824 Raft Lab2A Leader Election

news2024/11/29 7:54:53

#Lab2A - Leader Election

  • I. Source
  • II. My Code
  • III. Motivation
  • IV. Solution
    • S1 - 角色转换
    • S2 - 发起 RequestVote 拉票请求
    • S3 - 收到 RequestVote 的不同反应
    • S4 - 发送 AppendEntries 心跳包
    • S5 - 收到 AppendEntries 的不同反应
    • S6 - defs.go约定俗成和GetState()
  • V. Result

I. Source

  1. MIT-6.824 2020 课程官网
  2. Lab2: Raft 实验主页
  3. simviso 精品付费翻译 MIT 6.824 课程

II. My Code

  1. source code 的 Gitee 地址
  2. Lab2A: Leader Election 的 Gitee 地址

课程官网提供的 Lab 代码下载地址,我没有访问成功,于是我从 Github 其他用户那里 clone 到干净的源码,有需要可以访问我的 Gitee 获取

III. Motivation

提出 Raft 的主要目的,是为了解决容错问题,即使集群中有一些机器发生了故障,也不影响整体的运作(对外提供的服务)

我用一个 demo 来说明,假设我们的需求一直都是自己的 PC 能够顺利访问云端的资源(HTTP 或数据库)服务器。在服务器稳定在线的情况下,我们去访问它,一点问题都没有

但是,如果那唯一的一台服务器掉线了,那么我们将无法再访问,即对外的服务到此停止。这是我们无法忍受的,我们希望提供服务的一方能够保持稳定,时时刻刻为我提供访问服务。这就是我们的需求

好,现在问题摆在眼前,提供服务的一方怎样保证稳定性?让唯一的那台服务器永远维持稳定的状态,不允许宕机?这非常地不现实,就好比让一个人练成金刚不坏之身

所以,我们只能琢磨是否可以通过添加服务器的数量来确保对外服务的稳定。更近一步,即是现在服务器不再只有一台,扩充到 3 台,这 3 台中有一台是 primary 服务器,也主要由它对外提供服务;其他 2 台是 secondary 服务器(后备力量),拥有和 primary 服务器相同的数据内容

在 primary 服务器出现故障的时候,secondary 服务器顶上去,替代它的位置。这样就可以保持稳定的对外服务了

这就是我们应对资源服务器崩溃的最常用最有效的法子,但是想实现这个想法,首先要解决数据同步的问题,即如何确保 secondary 服务器拥有和 primary 服务器同样的内容?

这个同步问题,在学术上被称为共识算法,最经典的共识算法是 Paxos,但是它太难理解了。于是,斯坦福那帮人想出了更为简便的共识算法,即 Raft

通过 Raft 算法就可以同步集群中服务器的内容。要实现该算法,分三步走,5 - The Raft consensus algorithm 章节中的 Leader Election、Log Replication 和 Safety

本文主要针对第一步,Lab2A: Leader Election 展开讲解,集群只有选出了 leader,才能对外提供服务

IV. Solution

在讲解如何通过代码实现选举之前,先要清楚 Leader Election 的大致流程。只有知道了流程,才能开展编码工作

同样,学习的道理也是如此,只有先从理论上搞清楚这个东西的工作原理是怎样的,以及它是如何构建的,才知道自己应该如何动手

我很喜欢特斯拉的那种精神,他说,他在做实验之前,都会在脑中构思一下,想出蓝图,并反复推演每个细节。在理论上无误的情况下,才会开始实验。而且实验往往很快就能成功

他特别瞧不起爱迪生,嘲讽他一万次才点亮电灯。他大致说爱迪生做了一万次才成功,脑子完全就是浆糊,根本没有去思考为什么出错了,并且从一开始就没有好好地想想其中的原理

一万次才点亮,只能说瞎猫碰到了死耗子,运气好而已,其他的谈不上。很多学习上的事情,确实如此,是要有坚持的精神,但更要有确定正确方向的念头。好,下面正式开始

S1 - 角色转换

为了方便理解,我还是展示一下 Raft 节点主线程中的业务循环,在 raft.go:run() 中,

func (rf *Raft) run() {
	for !rf.killed() {
		switch rf.role {
		case Follower:
			select {
			case <-rf.grantVoteCh:
			case <-rf.heartBeatCh:
			case <-time.After(randElectionTimeOut()):
				rf.role = Candidate
			}
			break
		case Candidate:
			rf.mu.Lock()
			rf.curTerm++
			rf.votedFor = rf.me
			rf.voteCount = 1
			rf.mu.Unlock()

			go rf.boatcastRV()
			select {
			case <-time.After(randElectionTimeOut()):
			case id := <-rf.heartBeatCh:
				/* 一定是收到了来自集群中同期 OR 任期比它大的 leader 的心跳包 */
				rf.mu.Lock()
				rf.role = Follower
				rf.votedFor = id.int /* 被动回滚至 follower */
				rf.mu.Unlock()
			case <-rf.leaderCh:
				/* 赢得了选举 */
				rf.role = Leader
      }
			break
		case Leader:
			rf.boatcastAE()
			time.Sleep(fixedHeartBeatTimeOut())
			break
		}
	}
}

Raft 节点一直在待命着,反应在代码中就是最外层的 for 循环,它根据自己的角色做相应的事情。另外还需要介绍一下 Raft 结构体中的定义,

type Raft struct {
	mu        sync.Mutex          // Lock to protect shared access to this peer's state
	peers     []*labrpc.ClientEnd // RPC end points of all peers
	persister *Persister          // Object to hold this peer's persisted state
	me        int                 // this peer's index into peers[]
	dead      int32               // set by Kill()

	// Your data here (2A, 2B, 2C).
	// Look at the paper's Figure 2 for a description of what
	// state a Raft server must maintain.
	role      Role /* 三个角色 */
	voteCount int  /* 选票数 */
	curTerm   int  /* 任期号 */
	votedFor  int  /* 投给谁了 */

	grantVoteCh chan struct{}      /* 是否收到了拉票请求 */
	leaderCh    chan struct{}      /* 是否赢得了选举 */
	heartBeatCh chan struct{ int } /* 感知 leader 发来的心跳包 */
}

目前用于 Lab2A: Leader Election 的声明很简单,其中的 grantVoteCh 是用来感知身为 follower 的自己是否有收到拉票请求;leaderCh 是 candidate 用来感知自己是否赢得了选举;而 heartBeatCh 就很常见了,是 follower 和 candidate 用来感知自己是否收到了有效的心跳包。并在 raft.go:newRaft() 中定义其变量,

func newRaft(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {
	rf := &Raft{
		peers:       peers,
		persister:   persister,
		me:          me,
		role:        Follower,
		voteCount:   0,
		curTerm:     0,
		votedFor:    NoBody,
		grantVoteCh: make(chan struct{}, ChanCap),
		leaderCh:    make(chan struct{}, ChanCap),
		heartBeatCh: make(chan struct{ int }, ChanCap),
	}

	return rf
}

注意,这三个均为异步 channel,其容量为 ChanCap = 100

回归流程,follower 满脑子想当 leader,等待选举超时以便发起选举,同时还有两个对外的接口,grantVoteChheartBeatCh ,前者是 RequestVote() 中用来感知自己收否收到了有效的拉票请求;而后者是 AppendEntries() 中感知自己是否收到了有效的 leader 心跳包;如果收到了 candidate 有效的拉票申请或 leader 有效的心跳包,则重置自己的超时选举计时器

candidate 发起选举,将自己的任期加一并投票给自己;随后,立马发起投票,对应第 19 行的 go rf.boatcastRV() ;然后,线程进入 select 阻塞环节,一直在等待自己赢得选举的好消息 OR 收到集群中新 leader 的心跳包这个坏消息;如果这一轮的拉票没有结果,则继续开启新一轮的选举

而 leader 就简单很多了,成为 leader 就不会主动退位,除非它收到了来自新 leader 的有效心跳包。leader 就干一件事,即定期发送心跳包,对应第 34 行的 rf.boatcastAE()

这就是 Raft 节点之间的角色转换规则,如下图,

集群中每台机器,一开始的角色都是 follower,它们各自都有一个超时计时器,用来决定自己是否应该发起选举。何时应该发起选举呢?自然是等到计时器为 0,即超时了,才会主动跳出来竞争为 leader

而且,每个 follower 的计时器各不相同,是用随机值来设定超时时间的,这在论文的 5.2 - Leader election 中讲的很清楚,一般设为 150–300 ms。简单说一下,将超时时间设置成不同的值,主要是为了避免发生选票分散的情况

试想,现在集群中有 5 位 follower,大家的超时计时器值都相同,在同一时刻都倒计时为 0,都向着除了自己外的其他 4 台机器发起选票

这势必没有一个赢家,5 个人同时竞选,而且每人仅一张票,自己从 follower 变为 candidate 还要投自己一票,哪有功夫再去管别人。所以不可能有人会拿到过多数的选票

Raft 选用一种比较巧妙的随机超时值法,解决了上述的问题。在论文的 5.2 - Leader election 中提到了作者原先是想用排名系统来解决该问题的,但是鉴于该方案过于繁琐,不易理解,所以改用随机超时值的方法

回归正题,follower 发起选举,第一步就是将自己的角色从 follower 转变为 candidate,并且顺手将自己的任期加一。任期很好理解,跟现实世界一样,每个新皇帝刚上任,都会有一个新的年号。这个任期就可以理解成年号

随后,将立即向集群中的其他人发送拉票请求(RequestVote RPC) ,该 RPC 会包含一些重要的元数据,选民根据这些关于 candidate 的元数据来决定是否投票给它

如果 candidate 拿到了超过半数的选票,则直接当选为 leader;反之,则继续发起新的选举,所谓的 “新”,即任期也要累加增一

成为 leader 之后,就立马向集群发送心跳包以巩固自己的地位,raft.go:run() 中的第 33 行 case Leader 之后的代码

S2 - 发起 RequestVote 拉票请求

我们注意到在 Raft 节点成为 candidate 后会主动发起拉票请求,对应 raft.go:run() 的第 19 行 go rf.boatcastRV() ,其中的 go 意味着 rf.boatcastRV() 将会以新协程的方式开启,不会阻塞主线程的工作

这也很好理解,candidate 的拉票请求毕竟只是它自身的一部分,它还是要将重心放在监听自己是否有收到集群中有效心跳包的这件事上来。所以为了不影响这几件事情同时进行,这里选用了协程并发的思想来完成。看下其中的代码 raft.go:boatcastRV()

func (rf *Raft) boatcastRV() {
	rf.mu.Lock()
	args := RequestVoteArgs{
		Term:        rf.curTerm,
		CandidateId: rf.me,
	}
	rf.mu.Unlock()

	for i, _ := range rf.peers {
		if i != rf.me && rf.role == Candidate {
			go func(id int) {
				reply := RequestVoteReply{}
				rf.sendRequestVote(id, &args, &reply)
			}(i)
		}
	}
}

该方法做的事情很简单,就是向集群中的其他人发送拉票请求,即 sendRequestVote() 。值得注意的,上锁的颗粒度尽量要细,这样并发起来会更快。在这里我没采用 golang 风格的 defer 延迟释放锁,而是自己手动管理上锁放锁的事宜

我需要展示一下 RequestVote 两个一问一答的 RPC 的声明,

type RequestVoteArgs struct {
	// Your data here (2A, 2B).
	Term        int
	CandidateId int
}

type RequestVoteReply struct {
	// Your data here (2A).
	Term        int
	VoteGranted bool
}

其中第 4 行的 CandidateId 标明了参选人的编号,第 10 行的 VoteGranted 是为了告诉发起人,我是否认同你

另外,这个拉票请求方法翻译一下,即是广播 RequestVote,方法名应该为 broadcastRequestVote() 才对,我这里为了简短起见,选用了 boatcast 来代替 broadcastRequestVote 简写成 RV 。其中的 sendRequestVote() 的定义如下,

func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
	ok := rf.peers[server].Call("Raft.RequestVote", args, reply)

	rf.mu.Lock()
	defer rf.mu.Unlock()

	if !ok {
		return ok
	}

	term := rf.curTerm
	/* 自身过期的情况下,直接不再唱票 */
	if rf.role != Candidate || args.Term != term {
		return ok
	}

	/* 碰到一个任期比自己高的人 */
	if reply.Term > term {
		rf.curTerm = reply.Term
		rf.role = Follower /* candidate 主动回滚至 follower */
		rf.votedFor = NoBody
		return ok
	}

	if reply.VoteGranted {
		rf.voteCount++
		if rf.role == Candidate && rf.voteCount > len(rf.peers)/2 {
			rf.role = Leader /* 至关重要 */
			rf.leaderCh <- struct{}{}
		}
	}

	return ok
}

第 2 行会调用 Call() RPC 方法,在收到拉票应答时进行唱票。如果在收到票后发现自身已过期,则直接丢票;如果碰见比自己任期更高的选民,则直接回滚至 follower,这是在 S1 - 角色转换 中已经讲过的规则

我通过一个 demo 来讲解上段的代码。试想,现在集群中有 3 台机器。起初,大家都是 followers,A 先发制人,发起了选举,顺利收到了 B 和 C 的选票,成功地当选为 leader

从第 25 行开始说起,A 收到了 B 和 C 的成功回应,假设 B 的应答 RPC 先到达,A 根据第 25 行之后的逻辑,先累加选票数,然后再判断此时的票数是否已经过半

注意第 27 行的另一个条件,即 rf.role == Candidate ,candidate 要保证自身还是 candidate 的情况下,才会去唱票。反过来,如果 candidate 已经成为了 leader 或回滚至 follower,那么就无需再进行角色转换了。说得再直接点,A 收到了 B 的选票后,就已经满足了成为 leader 的条件(票数过半),可以直接忽略 C 的选票结果直接成为 leader 了

最难搞懂的地方,即第 28 行的角色转换操作必须要有,这是为了保证第 28、29 行的操作只执行一次!

有且仅执行一次,转换为选举问题,即是 A 在唱完 B 的选票后,就可以直接成为 leader 而忽略 C 的选票。这一步非常重要,要想通了

试想,如果 A 唱了 B 的票,那么 A 会在它自己的 leaderCh 中写入一个 struct{}{} 。而身为 candidate 的自己,在主线程中时时刻刻在监视着 sendRequestVote() 中的战况(是否选票过半?),一旦成功,它就会立刻成为 leader,结束 raft.go:run() 的 switch candidate 分支的流程,进入 switch leader 分支

在将第 28 行 rf.role == Leader 去掉的情况下,如果 A 在唱完 B 之后接着唱 C 的选票,那么会导致 rf.leaderCh <- struct{}{} 被再执行一次,即在这次选举期间已向 leaderCh 中写入了两个 struct{}{} ,这是非常致命的错误

我用个 demo 来证明,如果去掉第 28 行,那么整个流程就会有非常大的漏洞

假设,集群中还是 3 台机器,起初 A 先发制人成为了 leader,但是由于 A 的网络环境较差,断了联系脱离了集群( leader A 的任期为 1 );此时,B 和 C 发现集群中已无 leader,便都跃跃欲试;结果 B 当选 leader(任期为 2 );过段时间,A 又恢复连接上线了( A 的任期仍然为 1 ),它收到了来自更高任期 leader B 的心跳包后变为了 follower

但是,稳定的情况没维持多久,leader B 和 follower C 又双双掉线了,A 久久没收到了心跳包后,按耐不住要发起选举;成为 candidate 之后,A 发现 leaderCh 中还有一个 struct{}{} ,回想起来是第一次当选 leader 时,唱 B 和 C 两张票向 leaderCh 中写入了两个 struct{}{} 。虽然在第一次当选时读取了一个,但是还剩下一个,在这一次的选举中发挥作用,使得目前仅有一台服务器在线的集群选出了 leader,这本来就是个伪命题,仅有一台在线,它怎么可能获得过半的选票呢?

按照道理来说,即使 B、C 双双掉线,后来上线的 A 也不应该成为 leader!但是,A 靠着第一次当选积累的 leaderChstruct{}{} 却成功当选了

讲到这里,已经可以明白,我们不应该让 sendRequestVote() 两次写入 leaderCh 。可以通过第 27 行的 rf.role == candidate 和第 28 行的 rf.role = leader 角色转换来给写入操作加上限制,让这个写入操作仅做一次

这样,就可以避免上述提出的 candidate 快速当选 leader 的荒谬问题

S3 - 收到 RequestVote 的不同反应

如果 candidate 的任期小于自身,则直接拒绝为其投票;反之,就需要好好考虑了,且看一下代码,

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	// Your code here (2A, 2B).
	rf.mu.Lock()
	defer rf.mu.Unlock()

	/* 默认不投票 */
	reply.VoteGranted = false
	reply.Term = rf.curTerm

	if args.Term < rf.curTerm {
		return
	}

	/* 也可能是为了镇压任期较旧的 leader */
	if args.Term > rf.curTerm {
		rf.curTerm = args.Term
		rf.role = Follower
		rf.votedFor = NoBody /* 为臣服做准备 */
	}

	if rf.votedFor == NoBody || rf.votedFor == args.CandidateId {
		rf.role = Follower
		rf.votedFor = args.CandidateId /* 臣服于 leader */
		reply.VoteGranted = true
		rf.grantVoteCh <- struct{}{} /* 如果投票给他人,那么就需要重置自己的 ElectionTimeOut */
	}
}

最后决定该不该投票重点在第 21 行的条件判断,如 5 - The Raft consensus algorithm 的所讲到的,如果选民手里还有票 OR 已经投给你,则继续作为 follower 臣服于你

通过写入 grantVoteCh 告诉 Raft 主线程,当前我已收到了有效的拉票申请,恳请自己不要乱碰哒,继续老老实实作为 follower 即可

S4 - 发送 AppendEntries 心跳包

在 candidate 成为 leader 之后,要立刻通过 sendAppendEntries() 向集群中发送心跳包,以巩固自己的领导地位,对应 raft.go:run() 的第 34 行 rf.boatcastAE()

func (rf *Raft) boatcastAE() {
	rf.mu.Lock()
	args := AppendEntriesArgs{
		Term:     rf.curTerm,
		LeaderId: rf.me,
	}
	rf.mu.Unlock()

	for i, _ := range rf.peers {
		if i != rf.me && rf.role == Leader {
			go func(id int) {
				reply := AppendEntriesReply{}
				rf.sendAppendEntries(id, &args, &reply)
			}(i)
		}
	}
}

以及 sendAppendEntries() 的定义如下,

func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
	ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)

	rf.mu.Lock()
	defer rf.mu.Unlock()

	if !ok {
		return ok
	}

	term := rf.curTerm
	/* 自身过期的情况下,不需要在维护 nextIdx 了 */
	if rf.role != Leader || args.Term != term {
		return ok
	}

	/* 仅仅是被动退位,不涉及到需要投票给谁 */
	if reply.Term > term {
		rf.curTerm = reply.Term
		rf.role = Follower /* 主动回滚至 follower */
		rf.votedFor = NoBody
		return ok
	}

	return ok
}

和 S2 - 发起 RequestVote 拉票请求 一样,唯一需要注意的,就是 leader 的退位是被动的,只有它收到了任期比它还高的回绝时,它才会从 leader 回滚至 follower。换句话说,leader 不会因为 followers 不服它而自暴自弃

S5 - 收到 AppendEntries 的不同反应

和 S3 - 收到 RequestVote 的不同反应 差不多,

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	reply.Success = false
	reply.Term = rf.curTerm
	if args.Term < rf.curTerm {
		return
	}

	/* 心跳包只对 follower 和 candidate 管用,leader 是不会响应它的 */
	rf.heartBeatCh <- struct{ int }{args.LeaderId}
	/* 主要为了让旧 leader 收到了新 leader 的心跳包后而被迫退位 */
	if args.Term > rf.curTerm {
		rf.curTerm = args.Term
		rf.role = Follower
		rf.votedFor = NoBody
	}
	rf.votedFor = args.LeaderId /* 臣服于 leader,仅适用 Lab2A Leader election  */
	reply.Success = true
}

如果任期比自己的还小,则直接回绝;反之,则继续老老实实的作 follower。需要注意的可能情况,即是旧 leader 会收到新 leader 的心跳包,这时候就需要考虑被动退位的情况了,即对应代码中的第 14 行的情况。之后,还需要告诉 leader:我已臣服于你

另外,两个 AppendEntries RPC 的声明如下,

type AppendEntriesArgs struct {
	Term     int
	LeaderId int
}

type AppendEntriesReply struct {
	Term    int
	Success bool
}

S6 - defs.go约定俗成和GetState()

在构建 Raft 的过程中,会用到一些较为固定的值,比如角色、超时时间,我在 defs.go 中定义,

package raft

import (
	"math/rand"
	"time"
)

type Role int

const (
	NoBody           = -1
	Follower         = 0
	Candidate        = 1
	Leader           = 2
	ChanCap          = 100
	ElectionTimeOut  = 250 * time.Millisecond /* 要远大于论文中的 150-300 ms 才有意义,当然也要保证在 5 秒之内完成测试 */
	HeartBeatTimeOut = 100 * time.Millisecond /* 心跳 1 秒不超过 10 次 */
)

// 生成随机超时时间,在 250ms~500 ms 范围之内
func randElectionTimeOut() time.Duration {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	t := time.Duration(r.Int63()) % ElectionTimeOut
	return ElectionTimeOut + t
}

// 生成固定的心跳时间,固定值为 110 ms
func fixedHeartBeatTimeOut() time.Duration {
	return HeartBeatTimeOut
}

论文中虽然讲到,超时选举值在 150~300 ms 之间较为合适,但是 Lab2: Raft 实验主页 中提示我们不能这样做,设定的值要远大于论文中规定的值才有意义。原话是这样说的,

The paper’s Section 5.2 mentions election timeouts in the range of 150 to 300 milliseconds. Such a range only makes sense if the leader sends heartbeats considerably more often than once per 150 milliseconds. Because the tester limits you to 10 heartbeats per second, you will have to use an election timeout larger than the paper’s 150 to 300 milliseconds, but not too large, because then you may fail to elect a leader within five

并且,心跳时间也应该大于 100 ms,即原话的每秒钟心跳不超过 10 次

最后,还需要实现 raft.go:GetState() ,这是测试的接口,用来检测 Raft 节点的状态,

func (rf *Raft) GetState() (int, bool) {
	var term int
	var isleader bool
	// Your code here (2A).
	term = rf.curTerm
	isleader = rf.role == Leader

	return term, isleader
}

V. Result

golang 比较麻烦,它有 GOPATH 模式,也有 GOMODULE 模式,6.824-golabs-2020 采用的是 GOPATH,所以在运行之前,需要将 golang 默认的 GOMODULE 关掉,

$ export GO111MODULE="off"

随后,就可以进入 src/raft 中开始运行测试程序,

$ go test -run 2A

仅此一次的测试远远不够,可以通过 shell 循环,让测试跑个千把次,

$ for i in {1..1000}; go test -run 2A      

这样,如果还没错误,那应该是真的通过了。分布式的很多 bug 需要通过反复模拟才能复现出来的,它不像单线程程序那样,永远是幂等的情况

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

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

相关文章

The service already exists!

文章目录 项目场景&#xff1a;原因分析&#xff1a;解决方案&#xff1a; 项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 在给一位同学安装MySQL时报了这个错&#xff0c;我知道是她之前安装过但是没删干净的原因 但是我把Everything和注册表都查…

五、RGB实验(正点原子达芬奇Pro代码>>ZYNQ 7020代码移植)

RGB实验(正点原子达芬奇Pro代码&#xff1e;&#xff1e;ZYNQ 7020代码移植) 文章目录 RGB实验(正点原子达芬奇Pro代码&#xff1e;&#xff1e;ZYNQ 7020代码移植)前言一、本文目标二、移植步骤1.建立文件2.建立v文件1.lcd_rgb_colorbar2.lcd_driver3.rd_id4.clk_div5.lcd_dis…

单调队列算法模板及应用

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/algorithms-notes 】或者公众号【AIShareLab】回复 算法笔记 也可获取。 文章目录 队列算法模板例题&#xff1a;滑动窗口code 队列算法模板 // hh 表示队头&#xff0c;tt表示队尾 int q[N], hh 0…

使用Advanced Installer软件将winform程序打包成exe安装文件

在使用vs编写c#代码时&#xff0c;一般都是在debug文件中双击exe文件就可以执行&#xff0c;但是有时候需要将这个exe文件发给别人使用&#xff0c;在自己的电脑上exe文件可以执行&#xff0c;但是在别人的电脑上有时候打开后会报错&#xff0c;提示缺少.neta运行环境&#xff…

AUTUSAR通信篇 - CAN网络通信(一)

第一篇从全局角度出发&#xff0c;简单介绍了AUTOSAR的结构&#xff0c;从本篇开始我们一起详细了解一下AUTOSAR软件架构下内部的组成部分。下面&#xff0c;我们首先介绍第一个模块-通信。在AUTOSAR BSW中通信由三个部分组成&#xff0c;分别是&#xff1a;通信驱动、通信抽象…

【计算机视觉 | Pytorch】timm 包的具体介绍和图像分类案例(含源代码)

一、具体介绍 timm 是一个 PyTorch 原生实现的计算机视觉模型库。它提供了预训练模型和各种网络组件&#xff0c;可以用于各种计算机视觉任务&#xff0c;例如图像分类、物体检测、语义分割等等。 timm 的特点如下&#xff1a; PyTorch 原生实现&#xff1a;timm 的实现方式…

Java之线程池

目录 一.上节复习 1.阻塞队列 二.线程池 1.什么是线程池 2.为什么要使用线程池 3.JDK中的线程池 三.工厂模式 1.工厂模式的目的 四.使用线程池 1.submit()方法 2.模拟两个阶段任务的执行 五.自定义一个线程池 六.JDK提供线程池的详解 1.如何自定义一个线程池? 2.创…

【计网】第三章 数据链路层(3)信道划分介质访问控制

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 3.5-1 信道划分介质访问控制&#xff08;播报信道中应用&#xff09;一、传输数据使用的两种链路二、介质访问控制 三、信道划分 介质访问控制&#xff08;静态划分…

协程切换原理与实践 -- 从ucontext api到x86_64汇编

目录 1.协程切换原理理解 2.ucontext实现协程切换 2.1 实现流程 2.2 根据ucontext流程看协程实现 2.3 回答开头提出的问题 3.x86_64汇编实现协程切换 3.1libco x86_64汇编代码分析 3.2.保存程序返回代码地址流程 3.3.恢复程序地址以及上下文 4.实现简单协程框架 1.协程…

《编程思维与实践》1071.猜猜猜

《编程思维与实践》1071.猜猜猜 题目 思路 对于首字符而言,如果后一位字符与之相同,则首位选法只有1种,不同则2种; 对于最后一位字符而言,如果前一位字符与之相同,则末位选法只有1种,不同则2种; 对于中间的字符而言,有以下几种可能: 1.中间字符与前后字符均不同且前后字符不同…

企业挑选人力资源管理系统,需要从哪些角度考察?

企业在挑选人力资源管理系统时&#xff0c;除了要考虑到企业自身的主要需求外&#xff0c;还应该从哪些角度考察人力资源管理系统呢&#xff1f;一起来看看吧~ 一. 数据是否共通 企业在人力资源管理系统时通常有多个功能模块的需求。除了要看系统是否具备这些功能模块&#xff…

一分钟图情论文:《数据与信息之间逻辑关系的探讨——兼及DIKW概念链模式》

一分钟图情论文&#xff1a;《数据与信息之间逻辑关系的探讨——兼及DIKW概念链模式》 1989年&#xff0c;Ackoff R L在论文&#xff1a;《From data to wisdom》中正式提出DIKW概念链模型&#xff0c;在该模型提出后的20年间&#xff0c;在计算机学科、信息管理学科、图书情报…

数据结构--线段树

写在前面&#xff1a; 学习之前需要知道以下内容&#xff1a; 1. 递归 2. 二叉树 文章目录 线段树介绍用途建树修改单点修改区间修改 查询 代码实现。建树更新lazy传递查询 练习洛谷 P3372 【模板】线段树 1题目描述题解 线段树 介绍 线段树是一种二叉树&#xff0c;也可以…

【5G RRC】5G中的服务小区和邻区测量方法

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

STL配接器(容器适配器)—— stack 的介绍使用以及模拟实现。

注意 &#xff1a; 以下所有文档都来源此网站 &#xff1a; http://cplusplus.com/ 一、stack 的介绍和使用 stack 文档的介绍&#xff1a;https://cplusplus.com/reference/stack/stack/ 1. stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&…

Matlab进阶绘图第20期—带类别标签的三维柱状图

带类别标签的三维柱状图是一种特殊的三维柱状图。 与三维柱状图相比&#xff0c;带类别标签的三维柱状图通过颜色表示每根柱子的所属类别&#xff0c;从而可以更加直观地表示四维/四变量数据。 由于Matlab中未收录带类别标签的三维柱状图的绘制函数&#xff0c;因此需要大家自…

Java 使用 jdbc 连接 mysql

简介 Java JDBC 是 Java Database Connectivity 的缩写&#xff0c;它是一种用于连接和操作数据库的标准 API。Java JDBC 可以让 Java 程序通过 JDBC 驱动程序连接到各种不同类型的数据库&#xff0c;并且执行 SQL 语句来实现数据的读取、插入、更新、删除等操作。在本篇文章中…

Springboot整合Flowable流程引擎

文章目录 前言1. Flowable的主要表结构1.1 通用数据表&#xff08;通用表&#xff09;1.2运行时数据表&#xff08;runtime表&#xff09;1.3.历史数据表&#xff08;history表&#xff09;1.4. 身份数据表&#xff08;identity表&#xff09;1.5. 流程定义数据表&#xff08;r…

C++: 并行加速图像读取和处理的过程

文章目录 1. 目的2. 设计3. 串行实现4. 并行实现5. 比对&#xff1a;耗时和正确性6. 加速比探讨 1. 目的 读取单张图像&#xff0c;计算整图均值&#xff0c;这很好实现&#xff0c;运行耗时很短。 读取4000张相同大小的图像&#xff0c;分别计算均值&#xff0c;这也很好实现…

【OpenCv • c++】形态学技术操作 —— 开运算与闭运算

&#x1f680; 个人简介&#xff1a;CSDN「博客新星」TOP 10 &#xff0c; C/C 领域新星创作者&#x1f49f; 作 者&#xff1a;锡兰_CC ❣️&#x1f4dd; 专 栏&#xff1a;【OpenCV • c】计算机视觉&#x1f308; 若有帮助&#xff0c;还请关注➕点赞➕收藏&#xff…