[MIT6.824] Lab 3: Fault-tolerant Key/Value Service

news2024/11/19 19:16:46

[MIT6.824] Lab 3: Fault-tolerant Key/Value Service

目标

通过在Lab2中实现的Raft库,构建一个可容灾的KV数据库。

需要实现的服务有三种操作:

  • Put(key, value) key和value都是string,put设置指定key的value.

  • Append(key, arg) 将arg append到key对应的value。上,如果没有该key,相当于put操作。

  • Get(key) 返回值,如果没有该key,返回空字符串。

strong consistency

If called one at a time, the Get/Put/Append methods should act as if the system had only one copy of its state, and each call should observe the modifications to the state implied by the preceding sequence of calls.

需要实现linearizability.

实验介绍:

This lab has two parts. In part A, you will implement the service without worrying that the Raft log can grow without bound. In part B, you will implement snapshots

在A中实现基本功能, B中实现快照。

We supply you with skeleton code and tests in src/kvraft. You will need to modify kvraft/client.go, kvraft/server.go, and perhaps kvraft/common.go.

Part A: Key/value service without snapshots

Each of your key/value servers (“kvservers”) will have an associated Raft peer.

每个kv服务器对应一个raft peer。

Clerks send Put(), Append(), and Get() RPCs to the kvserver whose associated Raft is the leader.

clerk 将请求发送给raft leader之上的kvserver。然后kvserver将operation的log交给raft leader(通过之前的start)。

All of the kvservers execute operations from the Raft log in order, applying the operations to their key/value databases

回想之前的applymsg,kvserver通过raft,获取apply的log,并应用到kv数据库中。

A Clerk sometimes doesn’t know which kvserver is the Raft leader. If the Clerk sends an RPC to the wrong kvserver, or if it cannot reach the kvserver, the Clerk should re-try by sending to a different kvserver.

Clerk可能判断错误谁是raft的leader,如果发错了rpc,应当充实。

If the key/value service commits the operation to its Raft log (and hence applies the operation to the key/value state machine), the leader reports the result to the Clerk by responding to its RPC.

需要commit才能返回clerk的RPC.

If the operation failed to commit (for example, if the leader was replaced), the server reports an error, and the Clerk retries with a different server.

如果没有commit,应当report error(我认为此处是RPC携带错误信息)。

Your kvservers should not directly communicate; they should only interact with each other through Raft.

kvserver之前不应当直接交流,而是通过Raft库。(类似于网络分层)。

You’ll need to add RPC-sending code to the Clerk Put/Append/Get methods in client.go, and implement PutAppend() and Get() RPC handlers in server.go. These handlers should enter an Op in the Raft log using Start();

server.go中实现RPC的handler。

you should fill in the Op struct definition in server.go so that it describes a Put/Append/Get operation. Each server should execute Op commands as Raft commits them, i.e. as they appear on the applyCh. An RPC handler should notice when Raft commits its Op, and then reply to the RPC.

当raft提交op时,返回给RPC。

Client

You have completed this task when you reliably pass the first test in the test suite: “One client”.

在没有网络问题的情况下,首先开始实现只有一个客户端的测试了=。

首先看测试用例

func TestBasic3A(t *testing.T) {
	// Test: one client (3A) ...
	GenericTest(t, "3A", 1, 5, false, false, false, -1, false)
}		

开启一个客户端,5个服务器,没有RPC fail的情况,服务器不会崩溃,没有网络分区,不需要快照。

在测试里面,通过AppendGet(),Put()函数调用clerk的对应函数,并记录相应的log.

func Get(cfg *config, ck *Clerk, key string, log *OpLog, cli int) string {
    start := time.Now().UnixNano()
    v := ck.Get(key)
    end := time.Now().UnixNano()
    cfg.op()
    if log != nil {
       log.Append(porcupine.Operation{
          Input:    models.KvInput{Op: 0, Key: key},
          Output:   models.KvOutput{Value: v},
          Call:     start,
          Return:   end,
          ClientId: cli,
       })
    }

    return v
}

那么首先实现client.go/Clerk中的对应接口。

func (ck *Clerk) Get(key string) string {

    // You will have to modify this function.
    commandId := ck.lastAppliedCommandId + 1
    args := GetArgs{
       Key:       key,
       ClientId:  ck.clientId,
       CommandId: commandId,
    }

    serverId := ck.lastFoundLeader
    serverNum := len(ck.servers)
    for ; ; serverId = (serverId + 1) % serverNum {
       var reply GetReply
       ok := ck.servers[serverId].Call("KVServer.Get", &args, &reply)
       if !ok || reply.Err == ErrWrongLeader || reply.Err == ErrTimeout {
          continue
       }
       ck.lastFoundLeader = serverId
       ck.lastAppliedCommandId = commandId
       if reply.Err == ErrNoKey {
          return ""
       }
       return reply.Value
    }
}

因为不确定谁是leader,这里采用了循环的方式。如果响应超时,或者不是leader,尝试下一个服务器。

clerk调用了"KVServer.Get",接下来应当实现KVServer.Get

KVServer

type Op struct {
    // Your definitions here.
    // Field names must start with capital letters,
    // otherwise RPC will break.
    OpType    string
    Key       string
    Value     string
    ClientId  int64
    CommandId int
}

这里的Op用于交给Raft的log。

type ApplyResult struct {
	ErrMsg Err
	Value  string
	Term   int
}

ApplyResult是KV层提交所返回的消息。term用于判断该msg是否有效。

type CommandContext struct {
	CommandId int
	Reply     ApplyResult
}

CommandContext中,CommandId指示返回消息的指令序列号。

在KVServer中,先添加了三个字段

type KVServer struct {
    mu sync.Mutex
    me int
    rf *raft.Raft

    applyCh chan raft.ApplyMsg
    dead    int32 // set by Kill()

    maxraftstate int // snapshot if log grows this big

    // Your definitions here.
	clientReply map[int64]CommandContext
	replyChMap  map[int]chan ApplyResult // log的index对应的返回信息。
	lastApplied int
}

其中,clientReply存储每个client上一个已经执行完成操作的结果。

replyChMap的key是操作的index,对应一个接受消息的通道。

lastApplied是当前服务器的数据库上最后一次执行命令的index。

这里先判断Get Rpc中的Command是否已经被执行过。

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
    
    if clientReply, ok := kv.clientReply[args.ClientId]; ok {
       if args.CommandId == clientReply.CommandId {
          reply.Value = clientReply.Reply.Value
          reply.Err = clientReply.Reply.ErrMsg
          return
       } else if args.CommandId < clientReply.CommandId {
          reply.Err = ErrOutDate // 已经没有了之前执行的记录。
       }
    }

然后构造一个op,交给raft

op := Op{
    OpType:    GetOperation,
    Key:       args.Key,
    Value:     "",
    CommandId: args.CommandId,
    ClientId:  args.ClientId,
}
index, term, isLeader := kv.rf.Start(op)
if !isLeader {
    reply.Err = ErrWrongLeader
    return
}

接下来思考,raft的start是立即返回的,如何知道该op已经apply呢?这就要使用channel来等待了。可以先创建一个goroutine,用于在kvserver层接收raft层的apply消息,然后通过该apply的index消息,传给在Handler中开启的channel

replyCh := make(chan ApplyMsg)
kv.replyChMap[index] = replyCh
select {
case replyMsg := <-replyCh:
    {
       if term != replyMsg.Term {
          // 已经进入之后的term,leader改变(当前server可能仍然是leader,但是已经是几个term之后了)
          reply.Err = ErrWrongLeader
          return
       } else {
          reply.Value = replyMsg.Value
       }
    }
case <-time.After(KVTimeOut):
    {
       reply.Err = ErrTimeout
       return
    }
}

这里解释为什么不能接受term != replyMsg.Term

考虑这么一种情况:有三个服务器:A、B、C

首先A在Term1收到GetA请求,并交给RAFT层,index=1。但是此时leader从A变到B,第一个请求可能已经commit了,也可能没有发送给B,B受到另外一个客户端的请求Put(A,1),并且在term2成功提交。此时A的管道接收到index=1的提交信息,但是term改变了。RAFT只能保证term和index都相同时log相同,所以此时发生错误。

接下来考虑:什么时候在handler中创建的管道应当接收到信息呢?应该是在raft发送applyCh的时候。所以我们在KV服务器开始时应当kick out一个协程,专门用于处理applyCh的信息。

func (kv *KVServer) handleApplyMsg() {
    for !kv.killed() {
       applyMsg := <-kv.applyCh
       if applyMsg.CommandValid {
          go kv.applyCommand(applyMsg)
       }
       if applyMsg.SnapshotValid {
          go kv.applySnapshot(applyMsg)
       }
       _, err := DPrintf("Error Apply Msg: [%v]", applyMsg)
       if err != nil {
          return
       }
    }
}
func (kv *KVServer) applyCommand(msg raft.ApplyMsg) {
}

func (kv *KVServer) applySnapshot(msg raft.ApplyMsg) {
}

接下来实现applycommand

func (kv *KVServer) applyCommand(msg raft.ApplyMsg) {
    var applyResult ApplyResult
    op := msg.Command.(Op)
    applyResult.Term = msg.CommandTerm
    commandContext, ok := kv.clientReply[op.ClientId]
    if ok && commandContext.CommandId >= op.CommandId {
       // 该指令已经被应用过。
       // applyResult = commandContext.Reply
       return
    }
    switch op.OpType {
    case GetOperation:
       {
          value, ok := kv.kvdb.Get(op.Key)
          if ok {
             applyResult.Value = value
          } else {
             applyResult.Value = ""
             applyResult.ErrMsg = ErrNoKey
          }
       }
    case PutOperation:
       {
          value := kv.kvdb.Put(op.Key, op.Value)
          applyResult.Value = value
       }
    case AppendOperation:
       {
          value := kv.kvdb.Append(op.Key, op.Value)
          applyResult.Value = value
       }
    default:
       DPrintf("Error Op Type %s", op.OpType)
    }
    ch, ok := kv.replyChMap[msg.CommandIndex]
    if ok {
       ch <- applyResult
    }
    kv.clientReply[op.ClientId] = CommandContext{
       CommandId: op.CommandId,
       Reply:     applyResult,
    }
}

其中,kvdb就是用map操作实现的kv内存数据库。

调试

关于心跳选择

在[MIT6.824] Spring2021 Lab 2: Raft - AntiO2’s Blog中,我的RAFT写法是当收到log时不触发心跳,而是在等待固定间隔时发送心跳。这样就会导致TestSpeed3A中的操作过慢,因为客户端在上一条操作成功后才会进行下一条,导致但客户端时一条操作的时间必然大于心跳间隔,但是测试中要求一个心跳间隔至少进行3次操作。

所以还是需要在收到log时发送一条信息,但是这样又会导致多个client同时append log时,发送的RPC过多。

于是我想了如下优化:

首先是在TestPersistConcurrent3A测试中,采用每次接收新log都要发送一轮新消息的形式,共发送32104条log

… Passed – 21.7 5 32104 1379

在raft的ticker中修改

func (rf *Raft) ticker() {
	rf.mu.Lock()
	rf.setElectionTime()
	rf.mu.Unlock()
	// for append new log
	go func() {
		for rf.killed() == false {
			time.Sleep(rf.heartBeat / 30)
			rf.mu.Lock()
			if rf.status == leader {
				// 如果是leader状态,发送空包
				rf.setElectionTime()
				rf.mu.Unlock()
				rf.appendEntries(false)
				continue
			}
			rf.mu.Unlock()
		}
	}()
	for rf.killed() == false {

		// Your code here to check if a leader election should
		// be started and to randomize sleeping time using
		// time.Sleep().
		time.Sleep(rf.heartBeat)
		rf.mu.Lock()
		if rf.status == leader {
			// 如果是leader状态,发送空包
			rf.setElectionTime()
			rf.mu.Unlock()
			rf.appendEntries(true)
			continue
		}
		if time.Now().After(rf.electionTime) {
			// 如果已经超时, 开始选举
			go rf.startElection()
		}
		rf.mu.Unlock()
	}
}

相当于新增加了一个间隔为heartbeat/5的检测,如果有新的log就发送RPC请求,没有就不发送RPC(正常的heartbeat是无论有无新log都要发送)。

再次进行测试,

​ … Passed – 21.5 5 25873 1623

发送的RPC减少了,而且进行的操作数量也有所提升。(相当于进行了更高频的心跳,但是只有在有新log未同步时,才会触发这种高频心跳)

关于Data Race

在多客户端时,因为收到apply msg时,会启动一个apply command的go程,如果多个客户端同时get或put一个key,会导致data race.

解决方法:因为要严格按照log顺序apply,所以应当是直接调用apply command函数,而不是启动go程。

Part B: Key/value service with snapshots

The tester passes maxraftstate to your StartKVServer(). maxraftstate indicates the maximum allowed size of your persistent Raft state in bytes (including the log, but not including snapshots).

maxraftstate指示了Raft最大的logs大小。

You should compare maxraftstate to persister.RaftStateSize(). Whenever your key/value server detects that the Raft state size is approaching this threshold, it should save a snapshot using Snapshot, which in turn uses persister.SaveRaftState(). If maxraftstate is -1, you do not have to snapshot. maxraftstate applies to the GOB-encoded bytes your Raft passes to persister.SaveRaftState().

当raft的statesize快要接近maxraftstate时,使用snapshot制作快照。

首先实现状态机(KV数据库)生成和安装快照

func (kv *KvDataBase) GenSnapshot() []byte {
    w := new(bytes.Buffer)
    encode := gob.NewEncoder(w)
    err := encode.Encode(kv.KvData)
    if err != nil {
       return nil
    }
    return w.Bytes()
}
func (kv *KvDataBase) InstallSnapshot(data []byte) {
    if data == nil || len(data) < 1 { // bootstrap without any state?
       return
    }
    r := bytes.NewBuffer(data)
    encode := gob.NewDecoder(r)
    err := encode.Decode(&kv.KvData)
    if err != nil {
       panic(err.Error())
    }
}

在kvserver收到raft层的快照消息后,尝试将快照状态传递给raft,告诉它:我准备应用了。(如果是过期快照就不会应用),然后如果raft应用该快照,就安装。

func (kv *KVServer) applySnapshot(msg raft.ApplyMsg) {
	if kv.rf.CondInstallSnapshot(msg.SnapshotTerm, msg.SnapshotIndex, msg.Snapshot) {
		kv.lastAppliedIndex = msg.SnapshotIndex
		kv.kvdb.InstallSnapshot(msg.Snapshot)
	}
}

首先在内存的KV数据库中应用快照,然后告诉raft已经应用了快照。

接下来就要考虑生成快照的时机了。因为append log是并发的,在Start Op时判断是否生成快照感觉不太好,因为此时新的操作还没有被应用到数据库里面。

如果在apply里面生成快照,还能起到阻塞新的操作的作用。感觉是个挺好的选择。

我们还需要维护一个新的lastApplied int, 表示制作快照时已经应用的raft索引。kvserver在初始化时,应当将lastApplied初始化为snapshot所包含的最后一条索引。

kv.kvdb.Init(persister.ReadSnapshot())
kv.lastAppliedIndex = int(kv.rf.GetLastIncludeIndex())

然后在ApplyCommand中,增加制作快照。

kv.lastAppliedIndex = max(kv.lastAppliedIndex, msg.CommandIndex)
if kv.rf.GetRaftStateSize() > kv.maxraftstate {
    kv.rf.Snapshot(msg.CommandIndex, kv.kvdb.GenSnapshot())
}

调试

关于生成snapshot的时机

上面的几行代码已经足够通过3B的前几个测试,但是在TestSnapshotRecoverManyClients3B中出现了问题。

报错test_test.go:362: logs were not trimmed (56767 > 8*1000)

在多个客户端时,虽然成功生成了快照,但是出现了这么一种情况:

在apply一个新的操作后,发现raft的size过大,比如是10000,这个时候生成快照,但是新apply的index可能只往前推了几条,比如旧log有167条,其中只有5条被确认apply了,生成快照后还有162条在raft state中。

出现这种情况,可能是apply过慢, 因为snapshot本身要对数据库编码存储,需要耗费比较长的时间,所以不能每次apply都进行一次snapshot操作。

所以应当每隔一段时间进行snapshot操作,而不是每次applycommand都进行。比如目前commit index是100(假设超过了设定的max state),apply index 是0,使用定时器,就可以让kvserver快速apply command。而不是在每次apply之后进行snapshot操作。

需要修改raft代码

因为所有的kvserver都能独立生成snapshot,所以可能出现leader尝试发送的log已经不存在于follower的log中的情况,这种时候就需要判断哪些logs是有用的。

if args.PrevLogIndex < lastIncludeIndex {
    // 这里可能出现args.PrevLogIndex < lastIncludeIndex的情况
    // 如果所有的logs都很过时(已经包含在当前快照里面),就都不需要,否则保留新的。

    entryLen := len(args.Entries)

    if args.PrevLogIndex+IndexT(entryLen) <= lastIncludeIndex {
       rf.logger.Printf("[%d] receive outdated logs before snapshot", rf.me)
       reply.Success = true
       reply.Conflict = false
       rf.logLatch.RUnlock()
       return
    }
    trim := lastIncludeIndex - args.PrevLogIndex // 多余的部分
    // args.PrevLogIndex+IndexT(entryLen) > lastIncludeIndex
    //     IndexT(entryLen) > trim
    args.PrevLogIndex = args.Entries[trim-1].Index
    args.PrevLogTerm = args.Entries[trim-1].Term
    args.Entries = args.Entries[trim:]
}

因为这个问题在Lab2D中没有出现,所以当时没有发现这个bug。

snapshot的内容:

保存对client的历史回复也应当被保存在snapshot的快照中。

关于重复的Put/Append

当putappend重复时,也应当返回。这样client才能更新commandid

因为在不可靠网络环境下,可能已经进行了更新操作,但是没有及时返回,clerk再次尝试请求,此时结果已经存放好了。

image-20231006164207915

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

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

相关文章

『Linux』Linux环境搭建 | 阿里云云服务器白嫖 | Xshell环境配置

&#x1f525;博客主页&#xff1a; 小羊失眠啦 &#x1f516;系列专栏&#xff1a; C语言、Linux &#x1f325;️每日语录&#xff1a;时间&#xff0c;都是公平的&#xff0c;不公平的&#xff0c;只是现在的自己&#xff0c;对未来的自己。 ❤️感谢大家点赞&#x1f44d;收…

大华城市安防系统平台任意文件下载漏洞

一、漏洞描述 大华城市安防监控系统平台是一款集视频、报警、存储、管理于一体的综合安防解决方案。该平台支持多种接入方式&#xff0c;包括网络视频、模拟视频、数字视频、IP电话、对讲机等。此外&#xff0c;该平台还支持多种报警方式&#xff0c;包括移动侦测、区域入侵、…

Geteway

大家好我是苏麟今天带来Geteway. Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;该项目是基于 Spring 5.0&#xff0c;Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关&#xff0c;它旨在为微服务架构提供一种简单…

【C++】STL详解(十一)—— unordered_set、unordered_map的介绍及使用

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C学习 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C】STL…

ROS机械臂开发-开发环境搭建【一】

目录 前言环境配置docker搭建Ubuntu环境安装ROS 基础ROS文件系统 bugs 前言 想系统学习ROS&#xff0c;做一些机器人开发。因为有些基础了&#xff0c;这里随便写写记录一下。 环境配置 docker搭建Ubuntu环境 Dockerfile # 基础镜像 FROM ubuntu:18.04 # 设置变量 ENV ETC…

数据结构课程设计题目——链表综合算法设计、带头双向循环链表、插入、显示、删除、修改、排序

文章目录 链表综合算法设计——校园人事信息系统1.要求2.代码实现&#xff08;以带头双向循环链表为例&#xff09;2.1基本程序结构2.2节点和链表的初始化2.3链表的插入2.4链表的显示2.5链表的删除2.6链表的修改2.7链表的排序&#xff08;仿函数实现&#xff09; 3.全部源码 链…

Arxdbg读取cad扩展属性

xdlist可以读取CAD实体属性&#xff0c;子实体的扩展属性看不到。 下载arxdbg,appload加载。 鼠标右键即可看见&#xff0c;选Entity Info。 查看xdata,dxfdata都可以看到扩展信息。

Python 无废话-办公自动化Excel修改数据

如何修改Excel 符合条件的数据&#xff1f;用Python 几行代码搞定。 需求&#xff1a;将销售明细表的产品名称为PG手机、HW手机、HW电脑的零售价格分别修改为4500、5500、7500&#xff0c;并保存Excel文件。如下图 Python 修改Excel 数据&#xff0c;常见步骤&#xff1a; 1&…

jmeter和性能测试

一。性能测试的概念 1.性能&#xff1a;就是软件质量属性中的 “ 效率 ” 特性 2.效率特性&#xff1a; 时间特性&#xff1a;指系统处理用户请求的响应时间 资源特性&#xff1a;指系统在运行过程中&#xff0c;系统资源的消耗情况 CPU 内存 磁盘IO&#xff08;磁盘的写…

鸡群优化(CSO)算法(含MATLAB代码)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…

TCP VS UCP

程序员写网络程序&#xff0c;主要编写的应用层代码&#xff01; 真正要发这个数据&#xff0c;需要上层协议调用下层协议&#xff0c;应用层要调用传输层&#xff0c;则传输层给应用层提供一组api&#xff0c;统称为&#xff1a;soket api 基于UDP的api 基于TCP的api 这两个协…

Godot 初学

前言 因为9月份 Unity一顿安装计费的骚操作&#xff0c;导致世界开发者对于Unity 随意修改开发条例&#xff0c;追溯之前开发游戏版本感到愤怒。Unity是全球游戏使用率超过50%的引擎&#xff0c;Unity和Unreal是最主流的第三方游戏引擎。除非你是大厂可以自研引擎&#xff0c;…

阿里云服务器活动价格及配置整理表(多配置报价)

2023年阿里云服务器租用费用&#xff0c;阿里云轻量应用服务器2核2G3M带宽轻量服务器一年108元&#xff0c;2核4G4M带宽轻量服务器一年297.98元12个月&#xff0c;CS云服务器e系列2核2G配置182元一年、2核4G配置365元一年、2核8G配置522元一年&#xff0c;阿里云u1服务器2核4G、…

复习Day11:链表part04: 206. 反转链表、92. 反转链表II、25. K 个一组翻转链表、148. 排序链表

我用的方法是在leetcode再过一遍例题&#xff0c;明显会的就复制粘贴&#xff0c;之前没写出来就重写&#xff0c;然后从拓展题目中找题目来写。辅以Labuladong的文章看。然后刷题不用CLion了&#xff0c;使用leetcode自带的IDE模拟面试环境。 哈希表章节的题目思路很清晰&…

Java多线程之等待唤醒机制及案例代码演示

生产者和消费者&#xff08;等待唤醒机制&#xff09; 等待唤醒机制常见方法代码演示等待唤醒机制(阻塞队列方式实现)额外扩展 等待唤醒机制 生产者和消费者是一个十分经典的多线程协作模式 举个小栗子来说明一下消费者和生产者的等待唤醒过程: 常见方法 void wait() 当前…

因为计算机中找不到mfc140.dll无法启动修复步骤分享

mfc140.dll是Microsoft Foundation Class Library&#xff08;微软基础类库&#xff09;的一个组件&#xff0c;它是许多Windows应用程序&#xff08;尤其是使用MFC编写的程序&#xff09;所必需的动态链接库。MFC&#xff08;Microsoft Foundation Classes&#xff09;是一个用…

Pytorch笔记之分类

文章目录 前言一、导入库二、数据处理三、构建模型四、迭代训练五、模型评估总结 前言 使用Pytorch进行MNIST分类&#xff0c;使用TensorDataset与DataLoader封装、加载本地数据集。 一、导入库 import numpy as np import torch from torch import nn, optim from torch.uti…

10.5汇编语言整理

【汇编语言相关语法】 1.汇编语言的组成部分 1.伪操作&#xff1a;不参与程序的执行&#xff0c;但是用于告诉编译器程序该怎么编译 .text .global .end .if .else .endif .data 2.汇编指令 编译器将一条汇编指令编译成一条机器码&#xff0c;在内存里一条指令占4字节内存&…

c++---模板篇

1、模板 概念&#xff1a;模板就是建立通用的模具&#xff0c;大大提高复用性 特点&#xff1a; 模板不可以直接使用&#xff0c;它只是一个框架模板的通用并不是万能的 1.1、函数模板 C另一种编程思想称为泛型编程&#xff0c;主要利用的技术就是模板C提供两种模板机制&a…

数据结构与算法(四):哈希表

参考引用 Hello 算法 Github&#xff1a;hello-algo 1. 哈希表 1.1 哈希表概述 哈希表&#xff08;hash table&#xff09;&#xff0c;又称散列表&#xff0c;其通过建立键 key 与值 value 之间的映射&#xff0c;实现高效的元素查询 具体而言&#xff0c;向哈希表输入一个键…