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

news2024/11/17 10:37:10

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 = iota
    	MapTaskFailed
    	ReduceTaskCompleted
    	ReduceTaskFailed
    )
    
  • 对于Coordinator回复消息,Coordinator需要分配ReduceMap任务,告知任务的类型,或者告知Worker休眠(暂时没有任务需要执行)、Worker退出(所有任务执行成功)

    type TaskType int
    const (
    	MapTask = iota
    	ReduceTask
    	Wait
    	Exit
    )
    

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

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

回复消息结构如下:

type MessageReply struct {
	TaskID   int      // task id
	TaskType TaskType // task type, map or reduce or wait or exit
	TaskFile string   // task file name
	NReduce  int      // reduce number, indicate the number of reduce tasks
	NMap     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 = iota
	Assigned
	Completed
	Failed
)

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

type TaskInfo struct {
	TaskStatus TaskStatus // task status
	TaskFile   string     // task file
	TimeStamp  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 tasks
	NReduce                int        // number of reduce tasks
	MapTasks               []TaskInfo // map task
	ReduceTasks            []TaskInfo // reduce task
	AllMapTaskCompleted    bool       // whether all map tasks have been completed
	AllReduceTaskCompleted bool       // whether all reduce tasks have been completed
	Mutex                  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 {
	// lock
	c.Mutex.Lock()
	defer c.Mutex.Unlock()

	// assign map task
	if !c.AllMapTaskCompleted {
		// count the number of completed map tasks
		NMapTaskCompleted := 0
		for 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.TaskFile
				reply.TaskID = idx
				reply.TaskType = MapTask
				reply.NReduce = c.NReduce
				reply.NMap = c.NMap
				c.MapTasks[idx].TaskStatus = Assigned  // mark the task as assigned
				c.MapTasks[idx].TimeStamp = time.Now() // update the time stamp
				return nil
			} else if taskInfo.TaskStatus == Completed {
				NMapTaskCompleted++
			}
		}
		// check if all map tasks have been completed
		if NMapTaskCompleted == len(c.MapTasks) {
			c.AllMapTaskCompleted = true
		} else {
			reply.TaskType = Wait
			return nil
		}
	}

	// assign reduce task
	if !c.AllReduceTaskCompleted {
		// count the number of completed reduce tasks
		NReduceTaskCompleted := 0
		for idx, taskInfo := range c.ReduceTasks {
			if taskInfo.TaskStatus == Unassigned || taskInfo.TaskStatus == Failed ||
				(taskInfo.TaskStatus == Assigned && time.Since(taskInfo.TimeStamp) > 10*time.Second) {
				reply.TaskID = idx
				reply.TaskType = ReduceTask
				reply.NReduce = c.NReduce
				reply.NMap = c.NMap
				c.ReduceTasks[idx].TaskStatus = Assigned  // mark the task as assigned
				c.ReduceTasks[idx].TimeStamp = time.Now() // update the time stamp
				return nil
			} else if taskInfo.TaskStatus == Completed {
				NReduceTaskCompleted++
			}
		}
		// check if all reduce tasks have been completed
		if NReduceTaskCompleted == len(c.ReduceTasks) {
			c.AllReduceTaskCompleted = true
		} else {
			reply.TaskType = Wait
			return nil
		}
	}

	// all tasks have been completed
	reply.TaskType = Exit
	return 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 = Completed
		return nil
	} else if args.TaskCompletedStatus == MapTaskFailed {
		c.MapTasks[args.TaskID].TaskStatus = Failed
		return nil
	} else if args.TaskCompletedStatus == ReduceTaskCompleted {
		c.ReduceTasks[args.TaskID].TaskStatus = Completed
		return nil
	} else if args.TaskCompletedStatus == ReduceTaskFailed {
		c.ReduceTasks[args.TaskID].TaskStatus = Failed
		return 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 file
	file, err := os.Open(reply.TaskFile)
	if err != nil {
		log.Fatalf("cannot open %v", reply.TaskFile)
		return
	}
	// read the file, get the content
	content, 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 pairs
	kva := mapf(reply.TaskFile, string(content))
	// create intermediate files
	intermediate := make([][]KeyValue, reply.NReduce)
	for _, kv := range kva {
		r := ihash(kv.Key) % reply.NReduce
		intermediate[r] = append(intermediate[r], kv)
	}

	// write the intermediate files
	for 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 file
			enc.Encode(kv)
		}
		ofile.Close()
		// Atomic file renaming:rename the tempfile to the final intermediate file
		os.Rename(ofile.Name(), oname)
	}

	// send the task completion message to the coordinator
	args := 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 []string
	for 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 files
	var intermediate []KeyValue
	// get the intermediate file names
	intermediateFiles := 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 file
		dec := 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 key
	sort.Slice(intermediate, func(i, j int) bool {
		return intermediate[i].Key < intermediate[j].Key
	})

	// write the key-value pairs to the output file
	oname := 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 + 1
		for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
			j++
		}
		var values []string
		for k := i; k < j; k++ {
			values = append(values, intermediate[k].Value)
		}

		// call the reduce function to get the output
		output := reducef(intermediate[i].Key, values)

		// write the key-value pairs to the output file
		fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)

		i = j
	}
	ofile.Close()
	// rename the output file to the final output file
	os.Rename(ofile.Name(), oname)

	// send the task completion message to the coordinator
	args := 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.coloradmin.cn/o/1676306.html

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

相关文章

IPsec VPN简介

什么是IPsec&#xff1f; IPsec&#xff08;Internet Protocol Security&#xff09;是为IP网络提供安全性的协议和服务的集合&#xff0c;它是VPN&#xff08;Virtual Private Network&#xff0c;虚拟专用网&#xff09;中常用的一种技术。其实就是一种协议簇&#xff08;类…

linux 任务管理(临时任务定时任务) 实验

目录 任务管理临时任务管理周期任务管理 任务管理 临时任务管理 执行如下命令添加单次任务&#xff0c;输入完成后按组合键Ctrl-D。 [rootopenEuler ~]# at now5min warning: commands will be executed using /bin/sh at> echo "aaa" >> /tmp/at.log at&g…

中国目前比较有影响力的人物颜廷利:物质与无知通音

既然是在中国优秀传统文化之根-汉语当中&#xff0c;汉字‘物质’二字跟‘无知’通音&#xff0c;因此&#xff0c;面对当前金钱肆虐、物欲横流的现实生活&#xff0c;当人类众生把‘物质’&#xff08;无知&#xff09;生活看的太真、太重时&#xff0c;那么&#xff0c;这就很…

内网渗透—域信息收集防火墙策略同步不出网隧道上线

1、前言 最近在学域渗透方面的知识&#xff0c;这里就记录一下。主要涉及到什么是域&#xff0c;域的信息收集&#xff0c;防火墙的策略同步&#xff0c;以及当主机出网的协议被封杀时如何利用隧道上线。 2、什么是域 域是一个有安全边界的计算机集合&#xff0c;在域内的计…

AcqKnowledge 5.0使用方法

Biopac 数据导入 matlab 处理方法 第一步&#xff1a;在 AcqKnowledge 软件中&#xff0c;将数据通道的 mark 信息导入到 Graph&#xff0c;并将数据存储为 acq3 的格式 第二步&#xff1a;MATLAB中读取acq3文件脚本 clc clear %%%所有被试这一层路径 pathsub fullfile(file…

Milvus 简介与核心特性

一、Milvus 概述 Milvus 是一个开源的向量数据库&#xff0c;由 Zilliz 公司发起并维护。它专为处理非结构化数据而设计&#xff0c;能够存储、检索和分析大量的向量数据。Milvus 的名字来源于拉丁语&#xff0c;意为“一万”&#xff0c;象征着其处理大规模数据集的能力。 M…

【TypeScript】ts中的类型别名(Type)的介绍和使用

简言 类型别名(Type)的介绍和使用。 类型别名特别常用。 类型别名 类型别名就是用关键词type声明的类型&#xff1a; // Name类型 type Name string类型特别简单&#xff0c;他的作用就是定义一个类型的别名&#xff0c;这个类型可以是简单基础类型&#xff0c;也可以是复…

【QuikGraph】C#调用第三方库计算有向图、无向图的连通分量

QuikGraph库 项目地址&#xff1a;https://github.com/KeRNeLith/QuikGraph 相关概念 图论、连通分量、强连通分量相关概念&#xff0c;可以从其他博客中复习&#xff1a; https://blog.csdn.net/weixin_50564032/article/details/123289611 https://zhuanlan.zhihu.com/p/3…

buff禁售武器箱和胶囊的原因,及游戏搬砖人该如何应对

大家好&#xff0c;我是童话&#xff0c;相信大家都看到这个消息了&#xff0c;buff平台在14号中午11点左右&#xff0c;已经全面禁止了武器箱和胶囊&#xff0c;纪念包等的上架和售卖。在饰品市场直接搜索武器箱或者胶囊&#xff0c;是完全搜索不出来任何东西的哈。 面对这一消…

DOM重点核心(注册事件+DOM事件流)

目录 1.注册事件 注册时间概述 addEventListener() 删除事件 2.DOM事件流 DOM事件流理论 事件对象 事件对象的常见属性和方法 e.targe 和 this的区别 阻止默认行为 阻止冒泡 事件委托 禁止右键菜单和禁止选中文字 获得鼠标的坐标&#xff08;可视区、页面、浏览器…

VS Code 将输入的tab自动替换为空格

VS Code 将tab缩进修改为空格缩进TOC 位置 图1 图2 图3 图4 使能 同时勾选图2和图3的两个配置项&#xff0c;在编辑器中输入的Tab&#xff0c;会自动替换为 4(图4的Tab Size)个空格 检查 请参考 https://blog.csdn.net/de_se/article/details/131758698?ops_request_m…

短剧看剧系统,当前互联网热门项目工具系统模板。

目录 揭秘爆款神器&#xff1a;短剧看剧系统&#xff0c;让你的内容火遍全网&#xff01; 一、短剧看剧系统&#xff1a;一站式解决方案 二、灵活定价&#xff0c;实现收益最大化 三、高效管理&#xff0c;团队协作更轻松 四、数据驱动&#xff0c;精准把握市场动态 五、智…

Spring事务和事务传播机制(@Transactional)

文章目录 Spring事务&#xff08;Transactional详解&#xff09;什么是事务为什么需要事务事务的操作(sql) Spring中的事务实现Spring编程式事务Spring声明式事务 TransactionalTransactional可以用来修饰方法或类对异常进行捕获细节&#xff1a; Transactional详解三个属性1. …

思科模拟器学习1--Vlan Trunk

实验说明&#xff1a;将三台电脑的vlan 加到一台交换机里面&#xff0c;为了验证什么是虚拟局域网&#xff0c;把一个设备隔成三个空间&#xff0c;三个电脑互相不能通讯&#xff1b;目的是&#xff1a;vlan 1的通讯不可以向vlan 2传送&#xff0c;就是消息传送互不干扰的&…

node安装以及node的包管理工具

node安装以及node的包管理工具 node安装nvm管理工具 node安装 1、下载node链接: 点击这里 2、选择需要下载的版本以及对应的系统版本 nvm管理工具 1、下载nvm链接: 点击这里 2、双击运行.exe文件 全部默认安装即可 3、nvm常用命令 nvm list avaliable 显示所有可以下载的n…

QT——tableWidget-跳变之舞V1.0-记录学习【1】

QT——tableWidget-跳变之舞V1.0-记录学习【1】 文章目录 QT——tableWidget-跳变之舞V1.0-记录学习【1】前言一、利用QT创建项目文件1.1 完整项目文件如下图所示:1.2 演示&#xff1a; 二、声明文件&#xff1a;2.1 主界面声明文件:mainwindow.h&#xff1b;2.2 控制窗口声明文…

USB3.0接口——(3)协议层(包格式)

1.协议层 1.1.超高速传输事务 超高速事务&#xff08;SuperSpeed transactions&#xff09;由主机对设备端点请求或发送数据开始&#xff0c;并在端点发送数据或确认收到数据时完成。 超高速总线上的数据传输&#xff08;transfer&#xff09;是主机请求设备应用程序生成的数…

76岁林子祥升级做爷爷,亲自为孙女取名

林子祥与前妻吴正元的儿子&#xff0c;现年39岁的林德信入行以来绯闻不少&#xff0c;自与圈外女友Candace拍拖后便修心养性&#xff0c;去年他已经低调与拍拖5年多Candace完婚&#xff0c;正式步入人生另一阶段。 昨日&#xff08;5月12日&#xff09;林德信借母亲节这个温馨日…

如何在控制台应用程序里面托管ASP.NET Core网站

目录 介绍运行效果开发环境项目结构第三方库引用编写代码介绍 本文如何不通过IIS,使用控制台程序作为宿主主机来运行我们的asp.net core开发的网站程序。通过改案例你也可以后期通过winform或者wpf作为宿主来运行我们的asp.net core开发的网站。 运行效果 开发环境 vs2022+.…

2024年5月面试准备

2024年5月面试准备 资料来源Java基础泛型注解异常反射SPI机制Java集合CollectionMap 并发基础线程并发关键字并发集合Lock核心类并发集合核心类原子类核心类线程池核心类ScheduledThreadPoolExecutorForkJoinPoolFokJoinTask JUC原子类: CAS, Unsafe和原子类详解JUC 工具类 Jav…