Linux进程管理:(六)SMP负载均衡

news2024/10/7 18:28:40

文章说明:

  • Linux内核版本:5.0

  • 架构:ARM64

  • 参考资料及图片来源:《奔跑吧Linux内核》

  • Linux 5.0内核源码注释仓库地址:

    zhangzihengya/LinuxSourceCode_v5.0_study (github.com)

1. 前置知识

1.1 CPU管理位图

内核对CPU的管理是通过位图(bitmap)变量来管理的,并且定义了possible、present、online和active这4种状态:

/* 位图类型变量 */
// 表示系统中有多少个可以运行(现在运行或者将来某个时间点运行)的 CPU 内核
#define cpu_possible_mask ((const struct cpumask *)&__cpu_possible_mask)
// 表示系统中有多少个正处于运行(online)状态的 CPU 内核
#define cpu_online_mask   ((const struct cpumask *)&__cpu_online_mask)
// 表示系统中有多少个可处于运行状态的CPU内核,它们不一定都处于运行状态,有的CPU内核可能被热插拔了
#define cpu_present_mask  ((const struct cpumask *)&__cpu_present_mask)
// 表示系统中有多少个活跃的 CPU 内核
#define cpu_active_mask   ((const struct cpumask *)&__cpu_active_mask)
  • #define DECLARE_BITMAP(name,bits) \
    	unsigned long name[BITS_TO_LONGS(bits)]
    
    typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
    
    • name[]:位图类型变量使用一个长整数类型数组name[],每位代表一个CPU。对于64位处理器来说,一个长整数类型数组成员只能表示64个CPU内核。内核配置中有一个宏CONFIG_NR_CPUS为8,那么只需要一个长整数类型数组成员可。
    • struct cpumask:cpumask数据结构本质上是位图,内核通常使用cpumask的相关接口函数来管理CPU内核数量。

CPU位图的初始化流程如下图所示:

在这里插入图片描述

  • cpu_possible_mask通过查询系统DTS表或者ACPI表获取系统CPU内核数量
  • cpu_present_mask等同于cpu_possible_mask
  • cpu_active_mask是经过使能后的CPU内核数量

1.2 CPU调度域

根据系统的内存和高速缓存的布局,CPU域可分为如下几类:

在这里插入图片描述

Linux内核通过数据结构sched_domain_topology_level来描述CPU的层次关系,简称为SDTL:

// 用于描述 CPU 的层级关系
struct sched_domain_topology_level {
	sched_domain_mask_f mask;			// 函数指针,用于指定某个 SDTL 的 cpumask 位图
	// sd_flags 函数指针里指定了该调度层级的标志位
	sched_domain_flags_f sd_flags;		// 函数指针,用于指定某个 SDTL 的标志位
	int		    flags;
	int		    numa_level;
	struct sd_data      data;
#ifdef CONFIG_SCHED_DEBUG
	char                *name;
#endif
};

另外,内核默认定义了一个数组default_topology[]来概括CPU物理域的层次结构

// CPU 拓扑关系,从下往上
static struct sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT		// 超线程
	// cpu_smt_mask() 函数描述了 SMT 层级的 CPU 位图组成方式
	{ cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
#endif
#ifdef CONFIG_SCHED_MC		// 多核
	// cpu_coregroup_mask() 函数描述了 MC 层级的 CPU 位图组成方式
	{ cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif						// 处理器
	// cpu_cpu_mask() 函数描述了 DIE 层级的 CPU 位图组成方式
	{ cpu_cpu_mask, SD_INIT_NAME(DIE) },
	{ NULL, },
};

default_topology[]数组可知,系统默认支持DIE层级、SMT层级以及MC层级。另外,在调度域里划分调度组,使用sched_group来描述调度组,调度组是负载均衡调度的最小单位,在最低层级的调度域中,通常一个调度组描述一个CPU。

在一个支持NUMA架构的处理器中,假设它支持SMT技术,那么整个系统的调度域和调度组的关系如下图所示(它在默认调度层级中新增一个NUMA层级的调度域):

在这里插入图片描述

  • DIE表示处理器封装,它是CPU拓扑结构中的一个层级,在多核处理器中,通常有多个物理处理器核心,组成一个封装

在Linux内核中使用sched_domain数据结构来描述调度层级。在超大系统中系统会频繁访问调度域数据结构,为了提升系统的性能和可扩展性,调度域sched_domain数据结构采用Per-CPU变量来构建:

// 描述调度层级
struct sched_domain {
    // 父调度域指针,顶层调度域必须为 null 终止
    struct sched_domain *parent;
    // 子调度域指针,底层调度域必须为 null 终止
    struct sched_domain *child;
    // groups 指向该调度域的平衡组链表
    struct sched_group *groups;
    // 最小平衡间隔(毫秒)
    unsigned long min_interval;
    // 最大平衡间隔(毫秒)
    unsigned long max_interval;
    ...
};

下面举例来说明CPU调度域的拓扑关系,如下图所示,假设在一个4核处理器中,每个CPU拥有独立L1高速缓存且不支持超线程技术,4个物理CPU被分成两个簇Cluster0和Cluster1,每个簇包含两个物理CPU,簇中的CPU共享L2高速缓存

在这里插入图片描述

因为每个CPU内核只有一个运行线程,所以4核处理器没有SMT层级。簇由两个CPU组成,这两个CPU处于MC层级且是兄弟关系。整个处理器可以看作处于DIE层级,因此该处理器只有两个层级,即MC和DIE。根据上述原则,4核处理器的调度域和调度组的拓扑关系如下图所示:

在这里插入图片描述

在每个SDTL都为每个CPU分配了对应的调度域和调度组,以CPU0为例:

  • 对于DIE层级,CPU0对应的调度域是domain_die_0,该调度域管辖着4个CPU并包含两个调度组,分别为group_die_0和group_die_1
    • 调度组group_die_0管辖CPU0和CPU1
    • 调度组group_die_1管辖CPU2和CPU3
  • 对于MC层级,CPU0对应的调度域是domain_mc_0,该调度域中管辖着CPU0和CPU1并包含两个调度组,分别为group_mc_0和group_mc_1
    • 调度组group_mc_0管辖CPU0
    • 调度组group_mc_1管辖CPU1

除此以外,4核处理器的调度域和调度组还有两层关系,如下图所示:

在这里插入图片描述

综上所述,为了提升系统的性能和可扩展性,sched_domain数据结构采用Per-CPU变量来构建,这样可以减少CPU之间的访问竞争。而sched_group数据结构则是调度域内共享的。

2. LLC调度域

在负载均衡算法中,我们常常需要快速找到系统中最高层级的并且具有高速缓存共享属性的调度域。通常在—个系统,我们把最后一级高速缓存(Last Level Cache,LLC)称为LLC。在调度域标志位中,SD_SHARE_PKG_RESOURCE标志位用于描述高速缓存的共享属性。因此,在调度域层级中,包含LLC的最高一级调度域称为LLC调度域。在Linux内核中实现一组特殊用途的指针,用来指向LLC调度域,这些指针是Per-CPU变量。查找和设置LLC调度域是在update_top_cache_domain()函数中实现的。在select_idle_cpu()等函数中会使用到LLC 调度域。

// 指向 LLC 调度域
DEFINE_PER_CPU(struct sched_domain *, sd_llc);
// LLC 调度域包含多少个 CPU
DEFINE_PER_CPU(int, sd_llc_size);
// LLC 调度域第一个 CPU 的编号
DEFINE_PER_CPU(int, sd_llc_id);
DEFINE_PER_CPU(struct sched_domain_shared *, sd_llc_shared);
DEFINE_PER_CPU(struct sched_domain *, sd_numa);
DEFINE_PER_CPU(struct sched_domain *, sd_asym_packing);
// 指向第一个包含不同 CPU 架构的调度域,主要用于大/小核架构
DEFINE_PER_CPU(struct sched_domain *, sd_asym_cpucapacity);
DEFINE_STATIC_KEY_FALSE(sched_asym_cpucapacity);

// 查找和设置 LLC 调度域
static void update_top_cache_domain(int cpu)
{
    ...
}

3. 负载均衡

负载均衡机制从注册软中断开始,每次系统处理调度节拍时会检查当前是否需要处理负载均衡。负载均衡的流程如图所示:

在这里插入图片描述

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

load_balance()

// this_cpu 指当前正在运行的 CPU
// this_rq 表示当前就绪队列
// sd 表示当前正在做负载均衡的调度域
// idle 表示当前 CPU 是否处于 IDLE 状态
// continue_balancing 表示当前调度域是否还需要继续做负载均衡
static int load_balance(int this_cpu, struct rq *this_rq,
			struct sched_domain *sd, enum cpu_idle_type idle,
			int *continue_balancing)
{
	...

	struct lb_env env = {
		// 当前调度域
		.sd		= sd,
		// 当前的 CPU,后面可能要把一些繁忙的进程迁移到该 CPU 上
		.dst_cpu	= this_cpu,
		// 当前 CPU 对应的就绪队列
		.dst_rq		= this_rq,
		// 当前调度域里的第一个调度组的 CPU 位图
		.dst_grpmask    = sched_group_span(sd->groups),
		.idle		= idle,
		// 本次最多迁移 32 个进程(sched_nr_migrate_break全局变量的默认值为 32)
		.loop_break	= sched_nr_migrate_break,
		// load_balance_mask 位图
		.cpus		= cpus,
		.fbq_type	= all,
		.tasks		= LIST_HEAD_INIT(env.tasks),
	};

	// 把 sd 调度域管辖的 CPU 位图复制到 load_balance_mask 位图里
	cpumask_and(cpus, sched_domain_span(sd), cpu_active_mask);

	schedstat_inc(sd->lb_count[idle]);

redo:
	// should_we_balance() 函数判断当前 CPU 是否需要做负载均衡
	if (!should_we_balance(&env)) {
		*continue_balancing = 0;
		goto out_balanced;
	}

	// 查找该调度域中最繁忙的调度组
	group = find_busiest_group(&env);
	if (!group) {
		schedstat_inc(sd->lb_nobusyg[idle]);
		goto out_balanced;
	}

	// 在刚才找到的最繁忙的调度组中查找最繁忙的就绪队列
	busiest = find_busiest_queue(&env, group);
	if (!busiest) {
		schedstat_inc(sd->lb_nobusyq[idle]);
		goto out_balanced;
	}

	...

	// env.src_cpu 指向最繁忙的调度组中最繁忙的就绪队列中的 CPU
	env.src_cpu = busiest->cpu;
	// env.src_rq 指向最繁忙的就绪队列
	env.src_rq = busiest;

	ld_moved = 0;
	if (busiest->nr_running > 1) {
		...
		// detach_tasks() 函数遍历最繁忙的就绪队列中所有的进程,找出适合被迁移的进程,然后让这些
		// 进程退出就绪队列。cur_ld_moved 变量表示已经迁出了多少个进程
		cur_ld_moved = detach_tasks(&env);

		...

		if (cur_ld_moved) {
			// attach_tasks() 函数把刚才从最繁忙就绪队列中迁出的进程都迁入当前 CPU 的就绪队列中
			attach_tasks(&env);
			ld_moved += cur_ld_moved;
		}

		...
		// 由于进程设置了 CPU 亲和性,不能迁移进程到 dst_cpu,因此还有负载没迁移完成,需要换一个新的
		// dst_cpu 继续做负载迁移
		if ((env.flags & LBF_DST_PINNED) && env.imbalance > 0) {

			...
			goto more_balance;
		}

		...
	}

	...
}

load_balance()->should_we_balance()

// 判断当前 CPU 是否需要做负载均衡
static int should_we_balance(struct lb_env *env)
{
	// sg 指向调度域中的第一个调度组
	struct sched_group *sg = env->sd->groups;
	...
	// 查找当前调度组是否有空闲 CPU,如果有空闲 CPU,那么使用变量 balance_cpu 记录该 CPU
	for_each_cpu_and(cpu, group_balance_mask(sg), env->cpus) {
		if (!idle_cpu(cpu))
			continue;

		balance_cpu = cpu;
		break;
	}

	// 查找该调度组有哪些 CPU 适合做负载均衡
	// 	若调度组里有空闲 CPU,则优先选择空闲 CPU
	// 	调度组里没有空闲 CPU,则选择调度组的第一个 CPU
	if (balance_cpu == -1)
		balance_cpu = group_balance_cpu(sg);

	// 然后判断适合做负载均衡的 CPU 是否为当前 CPU,若为当前 CPU,则 should_we_balance()函数返回true,表示可以做负载均衡
	return balance_cpu == env->dst_cpu;
}

load_balance()->find_busiest_group()

// 查找该调度域中最繁忙的调度组
static struct sched_group *find_busiest_group(struct lb_env *env)
{
	...

	// 初始化 sd_lb_stats 结构体
	init_sd_lb_stats(&sds);

	// 更新该调度域中负载的相关统计信息
	update_sd_lb_stats(env, &sds);

	...
	// 如果没有找到最繁忙的调度组或者最繁忙的组中没有正在运行的进程,那么跳过该调度域
	if (!sds.busiest || busiest->sum_nr_running == 0)
		goto out_balanced;

	// 计算该调度域的平均负载
	sds.avg_load = (SCHED_CAPACITY_SCALE * sds.total_load)
						/ sds.total_capacity;

	// 如果最繁忙的调度组的组类型是 group_imbalanced,那么跳转到 force_balance 标签处
	if (busiest->group_type == group_imbalanced)
		goto force_balance;

	...
	// 若本地调度组的平均负载比刚才计算出来的最繁忙调度组的平均负载还要大,那么不需要做负载均衡
	if (local->avg_load >= busiest->avg_load)
		goto out_balanced;

	// 若本地调度组的平均负载比整个调度域的平均负载还要大,那么不需要做负载均衡
	if (local->avg_load >= sds.avg_load)
		goto out_balanced;

	// 如果当前 CPU 处于空闲状态,最繁忙的调度组里的空闲 CPU 数量大于本地调度组里的空闲 CPU 数量,说明不需要做负载均衡
	if (env->idle == CPU_IDLE) {
		if ((busiest->group_type != group_overloaded) &&
				(local->idle_cpus <= (busiest->idle_cpus + 1)))
			goto out_balanced;
	// 如果当前 CPU 不处于空闲状态,那么比较本地调度组的平均量化负载和最繁忙调度组的平均量化负载,这里使用了 imbalance_pct
	// 系数,它在 sd_init() 函数中初始化,默认值为125。若本地调度组的平均量化负载大于最繁忙组的平均量化负载,说明该调度组不
	// 忙,不需要做负载均衡。
	} else {
		if (100 * busiest->avg_load <=
				env->sd->imbalance_pct * local->avg_load)
			goto out_balanced;
	}

force_balance:
	env->src_grp_type = busiest->group_type;
	// 根据最繁忙调度组的平均量化负载、调度域的平均量化负载和本地调度组的平均量化负载来计算该调度域需要迁移的负载不均衡值
    calculate_imbalance(env, &sds);
	// 返回最忙的调度组
	return env->imbalance ? sds.busiest : NULL;

out_balanced:
	env->imbalance = 0;
	return NULL;
}

load_balance()->find_busiest_queue()

  1. for_each_cpu_and()遍历该调度组里所有的CPU
  2. 通过就绪队列的量化负载(runnable_load_avg)与CFS额定算力(cpu_capacity)的乘积来进行比较。使用weighted_cpuload()来获取cfs_rq->avg.runnable_load_avg的值,通过capacity_of()来获取cpu_capacity的值。这里要考虑不同处理器架构计算能力的不同对处理器负载的影响。

注意,数据结构rq中的cpu_capacity_orig成员指的是CPU的额定算力,而cpu_capacity成员指的是CFS就绪队列的额定算力,通常CFS额定算力最大能达到CPU额定算力的80%。

load_balance()->detach_tasks()

// detach_tasks() 函数遍历最繁忙的就绪队列中所有的进程,找出适合被迁移的进程,
// 然后让这些进程退出就绪队列。返回值表示已经迁出了多少个进程
static int detach_tasks(struct lb_env *env)
{
	...

	// env->imbalance 表示还有多少不均衡的负载需要迁移
	if (env->imbalance <= 0)
		return 0;

	// while 循环遍历最繁忙的就绪队列中所有的进程
	while (!list_empty(tasks)) {
		...

		// 在can_migrate_task()函数中判断哪些进程可以迁移,哪些进程不能迁移。
		// 不适合迁移的原因:
		//	一是进程允许运行的CPU位图的限制(cpus_allowed)
		//	二是当前进程正在运行
		//	三是热高速缓存(cache-hot)的问题
		//	四如果进程负载的一半大于要迁移负载总量(env->imbalance),则该进程也不适合迁移
		if (!can_migrate_task(p, env))
			goto next;

		load = task_h_load(p);

		if (sched_feat(LB_MIN) && load < 16 && !env->sd->nr_balance_failed)
			goto next;

		if ((load / 2) > env->imbalance)
			goto next;

		// detach_task()函数让进程退出就绪队列,然后设置进程的运行CPU为迁移目的地CPU,并设置p->on_rq为
		// TASK_ON_RQ_MIGRARING
		detach_task(p, env);
		// 把要迁移的进程添加到env->tasks链表中
		list_add(&p->se.group_node, &env->tasks);

		detached++;
		// 递减不均衡负载总量 env->imbalance
		env->imbalance -= load;
        ...
    }
    return detached;
}

load_balance()->attach_tasks()

// 把detach_tasks()分离出来的进程,重新添加到目标就绪队列中
static void attach_tasks(struct lb_env *env)
{
	...
	
    // 遍历env->tasks链表
	while (!list_empty(tasks)) {
		...
        // 把进程添加到目标CPU的就绪队列中
		attach_task(env->dst_rq, p);
	}

	...
}

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

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

相关文章

使用Java Runtime执行docker下的mysqldump

Runtime直接使用 docker exec mysql mysqldump -u%s -p%s cblog > %s&#xff08;%s是需要填充的数据&#xff09;&#xff0c;命令无法执行并且不会报错&#xff0c;需要使用字符串数组加入"sh", “-c”&#xff0c;具体代码示例&#xff1a; /*** MySQL数据备份…

无线局域网——wlan

目录 一.wlan的含义和发展 二.wlan技术带来的挑战 1.企业办公场景多样 2.位置速度的要求 3.安全的要求 4.规范的挑战 三.家庭和企业不同的部署需求 1.胖AP模式组网 2.AC瘦AP模式组网 3.组网模式的不同 四.三层隧道转发实验 1.拓扑 2.AP上线 核心交换机vlan ​编辑…

Magical Combat VFX

这个包包含30个可供游戏使用的VFX,有各种口味,为您的游戏增添趣味! 所有VFX都经过了很好的优化,可以在所有平台上使用。 这个包特别有一堆闪电魔法,有两种主要的变体,一种是深色的,另一种是浅色的。但它也提供了一系列其他视觉效果,如神圣咒语、音乐主题等等! 我们提供…

解决谷歌浏览器最新chrome94版本CORS跨域问题

项目场景&#xff1a; 谷歌浏览器升级到chrome94版本出现CORS跨域问题 问题描述 解决谷歌浏览器最新chrome94版本CORS跨域问题。 CORS跨域问题&#xff1a; 升级谷歌浏览器最新chrome94版本后&#xff0c;提示Access to XMLHttpRequest at ‘http://localhost:xxxx/api’ fro…

uni-app发布 h5 与ASP .NET MVC 后台 发布 到 IIS的同一端口 并配置跨域

iis 安装URL重写 选择对应的后台项目&#xff0c;进行url重写 编辑【模式】部分的内容的重写规则&#xff0c;我这里是h5中请求的前缀是api&#xff0c;大家可以根据自己的前缀进行修改。 编写【操作类型】为重写&#xff0c;并写重写url&#xff0c;按照图中设置即可。 uni…

使用Python构建RESTful API的最佳实践【第137篇—RESTful API】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 使用Python构建RESTful API的最佳实践 在当今的软件开发中&#xff0c;构建RESTful API已经…

【系统性】 循序渐进学C++

循序渐进学C 第一阶段&#xff1a;基础 一、环境配置 1.1.第一个程序&#xff08;基本格式&#xff09; ​ #include <iosteam> using namespace std;int main(){cout<<"hello world"<<endl;system("pause"); }​ 模板 #include &…

【开源鸿蒙】编译OpenHarmony轻量系统QEMU RISC-V版

文章目录 一、背景介绍二、准备OpenHarmony源代码三、准备hb命令3.1 安装hb命令3.2 检查hb命令 四、编译RISC-V架构的OpenHarmony轻量系统4.1 设置hb构建目标4.2 启动hb构建过程 五、问题解决5.1 hb set 报错问题解决 六、参考链接 开源鸿蒙坚果派&#xff0c;学习鸿蒙一起来&a…

【开源】SpringBoot框架开发二手车交易系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 二手车档案管理模块2.3 车辆预约管理模块2.4 车辆预定管理模块2.5 车辆留言板管理模块2.6 车辆资讯管理模块 三、系统设计3.1 E-R图设计3.2 可行性分析3.2.1 技术可行性分析3.2.2 操作可行性3.2.3 经济…

HarmonyOS NEXT应用开发—发布图片评论

介绍 本示例将通过发布图片评论场景&#xff0c;介绍如何使用startAbilityForResult接口拉起相机拍照&#xff0c;并获取相机返回的数据。 效果图预览 使用说明 通过startAbilityForResult接口拉起相机&#xff0c;拍照后获取图片地址。 实现思路 创建CommentData类&#…

vue ui 无反应解决方法

cmd 输入vue ui 无反应解决方法 在学习vuex的过程中&#xff0c;需要用到vue ui 建立项目&#xff0c;遇到的问题记录一下&#xff0c;希望能够以帮助有问题的其他人 查看vue版本 vue --version 或者 vue -V 发现vue版本才2.9.6 由于vue ui 命令需要vue3.0以上&#xff0c;…

如何实现图片上传至服务器

在绝大多数的项目中都会涉及到文件上传等&#xff0c;下面我们来说一下技术派中是如何实现原生图片上传的&#xff0c;这个功能说起来简单&#xff0c;但其实对于技术还是有考验的。图片的上传涉及到IO读写&#xff0c;一个文件上传的功能&#xff0c;就可以把IO流涉及到的知识…

Docker学习之数据管理(超详解析)

Docker存储资源类型&#xff1a; 用户在使用 Docker 的过程中&#xff0c;势必需要查看容器内应用产生的数据&#xff0c;或者需要将容器内数据进行备份&#xff0c;甚至多个容器之间进行数据共享&#xff0c;这必然会涉及到容器的数据管理&#xff1a; &#xff08;1&#xff…

网络安全之DDos攻击原理与防御

DDos简介 对于DDos(分布式拒绝服务)攻击&#xff0c;人们往往谈虎色变。它被认为是安全领域中最难解决的问题之一&#xff0c;迄今为止也没有完美的解决方案。DDos攻击是一种网络攻击方式&#xff0c;其目的是通过利用大量的互联网连接设备同时向目标网站或服务发送大量请求&a…

字母异位词分组【每日一题】

可以通过案例找到规律&#xff0c;每个词排序完后是同一个&#xff0c;所以通过hasmap存储排序过的值做key&#xff0c;值是存储单词集合。 package HasTable;import java.util.*;class Solution {static List<List<String>> groupAnagrams(String[] strs) {Map&l…

UnityShader(十六)凹凸映射

前言&#xff1a; 纹理的一种常见应用就是凹凸映射&#xff08;bump mapping&#xff09;。凹凸映射目的就是用一张纹理图来修改模型表面的法线&#xff0c;让模型看起来更加细节&#xff0c;这种方法不会改变模型原本的顶点位置&#xff08;也就是不会修改模型的形状&#xf…

《论文阅读》E-CORE:情感相关性增强的移情对话生成 EMNLP 2023

《论文阅读》E-CORE:情感相关性增强的移情对话生成 EMNLP 2023 前言摘要模型架构图构建边的构建和初始化节点的初始化图更新情感相关性加强解码损失函数总结前言 亲身阅读感受分享,细节画图解释,再也不用担心看不懂论文啦~ 无抄袭,无复制,纯手工敲击键盘~ 今天为大家带来…

Django验证码(一)

一、介绍 1.1、概述 验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序 可以防止:恶意破解密码、刷票、论坛灌水,有效防止某…

@Valid 和 @Validated 区别和使用方法

1、相关依赖 Valid 依赖包 <dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId> </dependency>Validated的依赖包 <dependency> <groupId>org.springframework</groupId> <…

学习JavaEE的日子 Day27 手撕HashMap底层原理

Day27 1.手撕HashMap底层原理(重点) public class Test01 {public static void main(String[] args) {// Float float1 new Float("0.0f"); // Float float2 new Float("0.0f"); // Float result float1/float2; // System.out.println(result);/…