MIT 6.5840(6.824) Lab1:MapReduce 设计实现

1 介绍

本次实验是实现一个简易版本的MapReduce,你需要实现一个工作程序(worker process)和一个调度程序(coordinator process)。工作程序用来调用Map和Reduce函数,并处理文件的读取和写入。调度程序用来协调工作任务并处理失败的任务。你将构建出跟 MapReduce论文 里描述的类似的东西。(注意:本实验中用"coordinator"替代里论文中的"master"。)

实验先决条件:

  • 阅读MapReduce论文

  • 阅读lab文档

  • 理解MapReduce框架

  • 理解原框架代码,理清所需完成任务

实验代码实现仓库:https://github.com/unique-pure/MIT6.5840/tree/main/src/mr,实验代码已通过实验测试,并在以下清单中列出了实现的功能及待办事项。

  • Complete the basic requirements for MapReduce
  • Handling worker failures
  • No data competition, a big lock ensures safety
  • Pass lab test
  • Communicate over TCP/IP and read/write files using a shared file system

2 原框架解析

  • src/mrapps/wc.go

    这是一个用于 MapReduce 的字数统计(Word Count)插件。该插件包含 Map 和 Reduce 函数,用于统计输入文本中的单词频率。

    func Map(filename string, contents string) []mr.KeyValue {// function to detect word separators.ff := func(r rune) bool { return !unicode.IsLetter(r) }// split contents into an array of words.words := strings.FieldsFunc(contents, ff)kva := []mr.KeyValue{}for _, w := range words {kv := mr.KeyValue{w, "1"}kva = append(kva, kv)}return kva
    }func Reduce(key string, values []string) string {// return the number of occurrences of this word.return strconv.Itoa(len(values))
    }
    
  • src/main/mrcoordinator.go

    mrcoordinator.go 定义了调度器(Coordinator)的主要逻辑。调度器通过 MakeCoordinator 启动一个 Coordinator 实例 c,并在 c.server() 中通过协程 go http.Serve(l, nil) 启动一个 HTTP 服务器来接收和处理 RPC 调用。

    func (c *Coordinator) server() {rpc.Register(c)rpc.HandleHTTP()//l, e := net.Listen("tcp", ":1234")sockname := coordinatorSock()os.Remove(sockname)l, e := net.Listen("unix", sockname)if e != nil {log.Fatal("listen error:", e)}go http.Serve(l, nil)
    }func MakeCoordinator(files []string, nReduce int) *Coordinator {c := Coordinator{}c.server()return &c
    }
    

    注意:在 Go 的 net/http 包中,使用 http.Serve(l, nil) 启动 HTTP 服务器时,服务器会为每个传入的请求自动启动一个新的协程。这意味着每个 RPC 调用都是在独立的协程中处理的,从而允许并发处理多个请求。因此,在设计时可能需要使用锁等同步原语来保护共享资源。此外,Coordinator 不会主动与 Worker 通信(除非额外实现),只能通过 Worker 的 RPC 通信来完成任务。同时,当所有任务完成时,Done 方法将返回 false,从而关闭 Coordinator。

  • src/main/mrworker.go

    mrworker.go 通过 Worker 函数运行。因此,Worker 函数需要完成请求任务、执行任务、报告任务状态等多个任务。可以推测,Worker 需要在这个函数中不断地轮询 Coordinator,并根据 Coordinator 的不同回复来驱动当前 Worker 完成各种任务。

  • src/main/mrsequential.go

    mrsequential.go 实现了一个简单的顺序 MapReduce 应用程序。该程序读取输入文件,执行 Map 和 Reduce 操作,并将结果写入输出文件。

    img

3 设计实现

3.1 任务分析

总体而言,Worker通过RPC轮询Coordinator请求任务,例如Map或者Reduce任务,Coordinator将剩余任务分配给Worker处理(先处理完Map任务才能处理Reduce任务)。

其中,在此实验中Map任务数量就是输入文件数量,每个Map Task的任务就是处理一个.txt文件;Reduce任务的数量是nReduce

由于Map任务会将文件的内容分割为指定的nReduce份,每一份应当由序号标明,拥有这样的序号的多个Map任务的输出汇总起来就是对应的Reduce任务的输入。

请求完任务后,Worker需要根据任务类型进行处理,这段处理过程跟mrsequential.go基本一致,但需要注意的就是论文中提到的,如果同一个任务被多个Worker执行,针对同一个最终的输出文件将有多个重命名操作执行。我们这就依赖底层文件系统提供的重命名操作的原子性来保证最终的文件系统状态仅仅包含一个任务产生的数据。即通过os.Rename()

处理完任务后,Worker通过RPC告知Coordinator任务结果。

所以,我们可以知道Coordinator管理着任务状态和任务分配,而无需记录Worker的信息,Worker实现任务处理。

整个任务流程如下图所示:

image-20240514154125349

MapReduce处理WordCount程序的流程如下图所示:

img

3.2 RPC

通信时首先需要确定这个消息是什么类型, 通过前述分析可知:

  • 对于Worker发送消息,Worker需要跟Coordinator报告MapReduce任务的执行情况(成功或失败)

    type TaskCompletedStatus int
    const (MapTaskCompleted = iotaMapTaskFailedReduceTaskCompletedReduceTaskFailed
    )
    
  • 对于Coordinator回复消息,Coordinator需要分配ReduceMap任务,告知任务的类型,或者告知Worker休眠(暂时没有任务需要执行)、Worker退出(所有任务执行成功)

    type TaskType int
    const (MapTask = iotaReduceTaskWaitExit
    )
    

同时,消息还需要附带额外的信息,我这里的设计是发送消息包含任务ID,以便Coordinator更新任务状态,结构如下:

type MessageSend struct {TaskID              int                 // task idTaskCompletedStatus TaskCompletedStatus // task completed status
}

回复消息结构如下:

type MessageReply struct {TaskID   int      // task idTaskType TaskType // task type, map or reduce or wait or exitTaskFile string   // task file nameNReduce  int      // reduce number, indicate the number of reduce tasksNMap     int      // map number, indicate the number of map tasks
}

这些字段都是为了辅助Worker进行任务处理,如NMap是为了提供Map任务的数量,以便生成中间文件名,TaskFile是保存Map任务需要处理的输入文件。

对于通信,原框架已提供Unix套接字通信,如果有想法,我们可以将 RPC 设置为通过 TCP/IP 而不是 Unix 套接字进行通信(请参阅 Coordinator.server() 中注释掉的行),并使用共享文件系统读/写文件。

3.2 Coordinator

3.2.1 结构

如前所述,Coordinator需要管理任务的状态信息,对于一个任务而言,我们这里定义它的状态为:未分配、已分配、完成、失败。

type TaskStatus int
const (Unassigned = iotaAssignedCompletedFailed
)

那么,任务结构应该包括任务状态,同时,如论文中提到的,可能有Worker成为落伍者,所以我们还需要考虑一个任务是否执行了很长时间还没结束,故这里需要记录任务分配时的时间戳,以便计算运行时间。另外,我们还需要一个字段来存储需要处理的任务文件名。故任务信息结构如下:

type TaskInfo struct {TaskStatus TaskStatus // task statusTaskFile   string     // task fileTimeStamp  time.Time  // time stamp, indicating the running time of the task
}

对于Coordinator结构,首先肯定是需要两个数据结构来存储所有的Map任务状态和Reduce任务状态,我这里使用的列表;然后由于是并发执行,更新共享任务状态数据,需要一把大锁保平安;最后需要一些额外变量存储任务数量(也可以直接len(list))以及标志某阶段任务是否完成(如在Reduce任务进行之前Map任务是否已经完成)。

type Coordinator struct {NMap                   int        // number of map tasksNReduce                int        // number of reduce tasksMapTasks               []TaskInfo // map taskReduceTasks            []TaskInfo // reduce taskAllMapTaskCompleted    bool       // whether all map tasks have been completedAllReduceTaskCompleted bool       // whether all reduce tasks have been completedMutex                  sync.Mutex // mutex, used to protect the shared data
}
3.2.2 初始化

我们需要对Coordinator初始化,其中最重要的是更新任务初始状态,一开始都是未分配,

func (c *Coordinator) InitTask(file []string) {for idx := range file {c.MapTasks[idx] = TaskInfo{TaskFile:   file[idx],TaskStatus: Unassigned,TimeStamp:  time.Now(),}}for idx := range c.ReduceTasks {c.ReduceTasks[idx] = TaskInfo{TaskStatus: Unassigned,}}
}
func MakeCoordinator(files []string, nReduce int) *Coordinator {c := Coordinator{NReduce:                nReduce,NMap:                   len(files),MapTasks:               make([]TaskInfo, len(files)),ReduceTasks:            make([]TaskInfo, nReduce),AllMapTaskCompleted:    false,AllReduceTaskCompleted: false,Mutex:                  sync.Mutex{},}c.InitTask(files)c.server()return &c
}
3.2.3 RequestTask函数

这部分比较复杂,根据我们之前的分析,处理逻辑如下:

  1. 如果有未分配的任务、之前执行失败、已分配但已经超时(10s)的Map任务,则选择这个任务进行分配;
  2. 如果以上的Map任务均不存在,但Map又没有全部执行完成,告知Worker先等待;
  3. Map任务全部执行完成的情况下,按照12相同的逻辑进行Reduce任务的分配;
  4. 所有的任务都执行完成了, 告知Worker退出。

因此,处理代码如下:

func (c *Coordinator) RequestTask(args *MessageSend, reply *MessageReply) error {// lockc.Mutex.Lock()defer c.Mutex.Unlock()// assign map taskif !c.AllMapTaskCompleted {// count the number of completed map tasksNMapTaskCompleted := 0for idx, taskInfo := range c.MapTasks {if taskInfo.TaskStatus == Unassigned || taskInfo.TaskStatus == Failed ||(taskInfo.TaskStatus == Assigned && time.Since(taskInfo.TimeStamp) > 10*time.Second) {reply.TaskFile = taskInfo.TaskFilereply.TaskID = idxreply.TaskType = MapTaskreply.NReduce = c.NReducereply.NMap = c.NMapc.MapTasks[idx].TaskStatus = Assigned  // mark the task as assignedc.MapTasks[idx].TimeStamp = time.Now() // update the time stampreturn nil} else if taskInfo.TaskStatus == Completed {NMapTaskCompleted++}}// check if all map tasks have been completedif NMapTaskCompleted == len(c.MapTasks) {c.AllMapTaskCompleted = true} else {reply.TaskType = Waitreturn nil}}// assign reduce taskif !c.AllReduceTaskCompleted {// count the number of completed reduce tasksNReduceTaskCompleted := 0for idx, taskInfo := range c.ReduceTasks {if taskInfo.TaskStatus == Unassigned || taskInfo.TaskStatus == Failed ||(taskInfo.TaskStatus == Assigned && time.Since(taskInfo.TimeStamp) > 10*time.Second) {reply.TaskID = idxreply.TaskType = ReduceTaskreply.NReduce = c.NReducereply.NMap = c.NMapc.ReduceTasks[idx].TaskStatus = Assigned  // mark the task as assignedc.ReduceTasks[idx].TimeStamp = time.Now() // update the time stampreturn nil} else if taskInfo.TaskStatus == Completed {NReduceTaskCompleted++}}// check if all reduce tasks have been completedif NReduceTaskCompleted == len(c.ReduceTasks) {c.AllReduceTaskCompleted = true} else {reply.TaskType = Waitreturn nil}}// all tasks have been completedreply.TaskType = Exitreturn nil
}
3.2.4 ReportTask函数

这个函数则是根据Worker发送的消息任务完成状态来更新任务状态信息即可,记住,一把大锁保平安

func (c *Coordinator) ReportTask(args *MessageSend, reply *MessageReply) error {c.Mutex.Lock()defer c.Mutex.Unlock()if args.TaskCompletedStatus == MapTaskCompleted {c.MapTasks[args.TaskID].TaskStatus = Completedreturn nil} else if args.TaskCompletedStatus == MapTaskFailed {c.MapTasks[args.TaskID].TaskStatus = Failedreturn nil} else if args.TaskCompletedStatus == ReduceTaskCompleted {c.ReduceTasks[args.TaskID].TaskStatus = Completedreturn nil} else if args.TaskCompletedStatus == ReduceTaskFailed {c.ReduceTasks[args.TaskID].TaskStatus = Failedreturn nil}return nil
}

3.3 Worker

3.3.1 Worker轮询

Worker需要通过RPC轮询Coordinator请求任务,然后根据返回的任务类型进行处理(即调用相应函数):

func Worker(mapf func(string, string) []KeyValue,reducef func(string, []string) string) {for {args := MessageSend{}reply := MessageReply{}call("Coordinator.RequestTask", &args, &reply)switch reply.TaskType {case MapTask:HandleMapTask(&reply, mapf)case ReduceTask:HandleReduceTask(&reply, reducef)case Wait:time.Sleep(1 * time.Second)case Exit:os.Exit(0)default:time.Sleep(1 * time.Second)}}
}
3.3.2 处理Map任务

mrsequential.go处理基本一致,处理完成后需要通过RPC告知Coordinator结果。但需要注意的是,我们需要通过os.Rename()原子重命名来保证最终的文件系统状态仅仅包含一个任务产生的数据。

func HandleMapTask(reply *MessageReply, mapf func(string, string) []KeyValue) {// open the filefile, err := os.Open(reply.TaskFile)if err != nil {log.Fatalf("cannot open %v", reply.TaskFile)return}// read the file, get the contentcontent, err := io.ReadAll(file)if err != nil {log.Fatalf("cannot read %v", reply.TaskFile)return}file.Close()// call the map function to get the key-value pairskva := mapf(reply.TaskFile, string(content))// create intermediate filesintermediate := make([][]KeyValue, reply.NReduce)for _, kv := range kva {r := ihash(kv.Key) % reply.NReduceintermediate[r] = append(intermediate[r], kv)}// write the intermediate filesfor r, kva := range intermediate {oname := fmt.Sprintf("mr-%v-%v", reply.TaskID, r)ofile, err := os.CreateTemp("", oname)if err != nil {log.Fatalf("cannot create tempfile %v", oname)}enc := json.NewEncoder(ofile)for _, kv := range kva {// write the key-value pairs to the intermediate fileenc.Encode(kv)}ofile.Close()// Atomic file renaming:rename the tempfile to the final intermediate fileos.Rename(ofile.Name(), oname)}// send the task completion message to the coordinatorargs := MessageSend{TaskID:              reply.TaskID,TaskCompletedStatus: MapTaskCompleted,}call("Coordinator.ReportTask", &args, &MessageReply{})
}
3.3.3 处理Reduce任务

这里利用我们生成的中间文件名特点,对于每个Reduce任务,它的输入文件(中间文件)名为mr-MapID-ReduceID,所以我们构造出输入文件数组,将其解码得到键值对,再进行处理。

// generate the intermediate files for reduce tasks
func generateFileName(r int, NMap int) []string {var fileName []stringfor TaskID := 0; TaskID < NMap; TaskID++ {fileName = append(fileName, fmt.Sprintf("mr-%d-%d", TaskID, r))}return fileName
}func HandleReduceTask(reply *MessageReply, reducef func(string, []string) string) {// load the intermediate filesvar intermediate []KeyValue// get the intermediate file namesintermediateFiles := generateFileName(reply.TaskID, reply.NMap)// fmt.Println(intermediateFiles)for _, filename := range intermediateFiles {file, err := os.Open(filename)if err != nil {log.Fatalf("cannot open %v", filename)return}// decode the intermediate filedec := json.NewDecoder(file)for {kv := KeyValue{}if err := dec.Decode(&kv); err == io.EOF {break}intermediate = append(intermediate, kv)}file.Close()}// sort the intermediate key-value pairs by keysort.Slice(intermediate, func(i, j int) bool {return intermediate[i].Key < intermediate[j].Key})// write the key-value pairs to the output fileoname := fmt.Sprintf("mr-out-%v", reply.TaskID)ofile, err := os.Create(oname)if err != nil {log.Fatalf("cannot create %v", oname)return}for i := 0; i < len(intermediate); {j := i + 1for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {j++}var values []stringfor k := i; k < j; k++ {values = append(values, intermediate[k].Value)}// call the reduce function to get the outputoutput := reducef(intermediate[i].Key, values)// write the key-value pairs to the output filefmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)i = j}ofile.Close()// rename the output file to the final output fileos.Rename(ofile.Name(), oname)// send the task completion message to the coordinatorargs := MessageSend{TaskID:              reply.TaskID,TaskCompletedStatus: ReduceTaskCompleted,}call("Coordinator.ReportTask", &args, &MessageReply{})
}

4 测试和常见问题

test-mr.sh为测试脚本,也可以通过运行sh test-mr-many.sh n来运行 n n n次测试。

bash test-mr.sh
*** Starting wc test
--- wc test: PASS
*** Starting indexer test.
--- indexer test: PASS
*** Starting map parallelism test.
--- map parallelism test: PASS
*** Starting reduce parallelism test.
--- reduce parallelism test: PASS
*** Starting job count test.
--- job count test: PASS
*** Starting early exit test.
--- early exit test: PASS
*** Starting crash test.
--- crash test: PASS
*** PASSED ALL TESTS

常见的问题如下:

  1. 不能通过job-count测试

    *** Starting job count test.
    --- map jobs ran incorrect number of times (10 != 8)
    --- job count test: FAIL
    

    因为多次处理同一个任务,且任务没有异常。这是因为在分配任务后没有更新任务的状态,例如标记为已分配和记录当前时间戳。

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

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

相关文章

晶振在电子设备中的作用是什么?

在无源晶振电路中&#xff0c;并联电阻起着至关重要的作用。无源晶振本身不能自行产生振荡&#xff0c;因此需要借助外部电路来实现。并联在晶振两端的电阻&#xff0c;通常称为负载电阻&#xff0c;对电路的稳定性和振荡性能有着重要影响。 晶振电路的核心是皮尔斯振荡器&…

mysql根据字段值关联查不同表

mysql根据字段值关联查不同表&#xff1a; 实现&#xff1a; 使用left join 结合case when 判断直接取值&#xff1a; select mp.member_id ,mp.store_id, case mp.store_type when 1 then bs.store_namewhen 2 then sc.store_namewhen 3 then be.store_name end as store_na…

string类篇超超超详解,40余个成员函数详细解释(图文)!看完包会!!

本篇目标 constructoroperatorElements accessIteratorsCapacityModifiersString operationsmember contants其他函数 一、constructor(对象的创建) void StrTest1() {string s1;//直接构造cout << s1 << endl;//string里内置了流插入、流提取的函数重载&#xf…

Naive RAG 、Advanced RAG 和 Modular RAG 简介

简介&#xff1a; RAG&#xff08;Retrieval-Augmented Generation&#xff09;系统是一种结合了检索&#xff08;Retrieval&#xff09;和生成&#xff08;Generation&#xff09;的机制&#xff0c;用于提高大型语言模型&#xff08;LLMs&#xff09;在特定任务上的表现。随…

深入解析RedisJSON:在Redis中直接处理JSON数据

码到三十五 &#xff1a; 个人主页 JSON已经成为现代应用程序之间数据传输的通用格式。然而&#xff0c;传统的关系型数据库在处理JSON数据时可能会遇到性能瓶颈。为了解决这一问题&#xff0c;Redis推出了RedisJSON模块&#xff0c;它允许开发者在Redis数据库中直接存储、查询…

产品推荐 | 基于 AMD Virtex UltraScale FPGA VCU1287 的特性描述套件

01 产品概述 VCU1287 功能描述套件可为您提供描述和评估 Virtex™ UltraScale™ XCVU095-FFVB2104E FPGA 上可用 32 GTH (16Gbps) 和 32 GTY (30Gbps) 收发器所需的一切功能。每个 GTH 与 GTY Quad 及其相关参考时钟均从 FPGA 路由至 SMA 及 Samtec BullsEye 连接器。 Bulls…

好题总结汇总

好题总结汇总 总结一些做完很有收获的题。 一、经典问题 DP的结合 1、题意&#xff1a; 给定 n n n 种颜色的球的数量 a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1​,a2​,...,an​&#xff0c;选出一些不同种类的球(也就是在n种球中选球的任意情况)&#xff0c;将球…

TCP的滑动窗口机制和流量控制

目录 滑动窗口 流量控制 拥塞控制 滑动窗口 TCP除了保证可靠性之外&#xff0c;也希望能够尽可能高效的完成数据传输。滑动窗口就是一种提高效率的机制。以下是不引入滑动窗口的数据传输过程&#xff1a; 可以看到&#xff0c;主机A这边每次收到一个ACK才发送下一个数据。这…

为什么cca门限和tx 功率有关系

Cca是用来决定信道是否繁忙&#xff0c;好像只和收有关。 但是为什么和tx有关。 设想一下这个网路布局。 如果某个STA在决定是否发送的时候&#xff0c;是否不能只看收到的干扰多大&#xff0c;还应该“冒险”一下&#xff0c;如果自己的功率足够&#xff0c;那么就可以扛住干…

MyBatis的注解实现复杂映射开发

xml 配置方式实现复杂映射回顾 ​ 实现复杂映射我们之前可以在映射文件中通过配置来实现&#xff0c;使用注解开发后&#xff0c;我们可以通过 Results 注解&#xff0c;Result 注解&#xff0c;One 注解和 Many 注解组合完成复杂关系的配置。 注解说明Results代替的是标签 …

【csv-parse】使用parse方法的时候来转换为csv字符串时,会导致输出有乱码

&#x1f601; 作者简介&#xff1a;一名大四的学生&#xff0c;致力学习前端开发技术 ⭐️个人主页&#xff1a;夜宵饽饽的主页 ❔ 系列专栏&#xff1a;前端bug记录 &#x1f450;学习格言&#xff1a;成功不是终点&#xff0c;失败也并非末日&#xff0c;最重要的是继续前进…

Vue3组件库开发项目实战——02项目搭建(配置Eslint/Prettier/Sass/Tailwind CSS/VitePress/Vitest)

摘要&#xff1a;在现代前端开发中&#xff0c;构建一个高效、可维护且易于协作的开发环境至关重要。特别是在开发Vue3组件库时&#xff0c;我们需要确保代码的质量、一致性和文档的完整性。本文将带你从0搭建vue3组件库开发环境&#xff0c;以下是配置代码规范、格式化、CSS样…

扩散模型diffusion model

一 什么是扩散模型 1.1 现有生成模型 已经有大量的方法证明深度生成模型能够模拟人类的想象思维&#xff0c;生成人类难以分辨真伪的内容&#xff0c;主要方法如下&#xff1a; 1、GAN&#xff1a;用神经网络训练生成器和判别器 GAN 的主要思想&#xff1a; GAN 就是一个互搏的…

49.乐理基础-拍号的类型-单拍子、复拍子

当前写的东西&#xff0c;如果只是想要看懂乐谱的话&#xff0c;它是没什么意义的&#xff0c;就像我们要把 0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5。。。称为自然数&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5称为正整…

H5 云商城 file.php 文件上传致RCE漏洞复现

0x01 产品简介 H5 云商城是一个基于 H5 技术的电子商务平台,旨在为用户提供方便快捷的在线购物体验。多平台适配:H5 云商城采用 H5 技术开发,具有良好的跨平台适配性。无论是在电脑、手机还是平板等设备上,用户都可以通过网页浏览器访问和使用云商城,无需安装额外的应用程…

销量?模糊销量?精准销量?如何获取淘宝商品销量数据接口

淘宝爬虫商品销量数据采集通常涉及以下几个步骤&#xff1a; 1、确定采集目标&#xff1a;需要明确要采集的商品类别、筛选条件&#xff08;如天猫、价格区间&#xff09;、销量和金额等数据。例如&#xff0c;如果您想了解“小鱼零食”的销量和金额&#xff0c;您需要设定好价…

vueday1

1.作用&#xff1a;利用表达式进行插值&#xff0c;渲染到页面中 三元表达式、点语法、数组对应项&#xff0c;方法、点语法方法 1.使用的时候需要存在&#xff0c;需要在data里面声明&#xff0c;如果没有声明会报错 2.ifelse不能用 3.不能在标签属性中使用{{}}插值 <!DO…

【前端】打砖块游戏:实现细节介绍

打砖块游戏:实现细节介绍 在本文中,我将详细介绍如何使用HTML、CSS和JavaScript技术构建一个简单的打砖块游戏。我们将重点讨论游戏的三个核心技术方面:碰撞检测、画图和事件监听。 完整代码我放在:github可以直接拉取代码测试。 游戏概览 打砖块游戏中,玩家通过控制底…

静态住宅IP优缺点总结

在进行海外 IP 代理时&#xff0c;了解动态住宅 IP 和静态住宅 IP 的区别以及如何选择合适的类型非常重要。本文将介绍精态住宅 IP 特点和&#xff0c;并提供选择建议&#xff0c;帮助您根据需求做出明智的决策。 静态住宅 IP 的特点 静态住宅 IP 是指 IP 地址在一段时间内保…

涨点神器:即插即用特征融合模块!超低参数,性能依旧SOTA

在写论文时&#xff0c;一些通用性模块可以在不同的网络结构中重复使用&#xff0c;这简化了模型设计的过程&#xff0c;帮助我们加快了实验的迭代速度。 比如在视觉任务中&#xff0c;即插即用的特征融合模块可以无缝集成到现有网络中&#xff0c;以灵活、简单的方式提升神经…