当前位置: 首页 > news >正文

C# 游戏引擎中的协程

前言

        书接上回,我谈到了Unity中的协程的重要性,虽然协程不是游戏开发“必要的”,但是它可以在很多地方发挥优势。

        为了在Godot找回熟悉的Unity协程开发手感,不得不自己做一个协程系统,幸运的是,有了Unity的开发经验和协程使用模型,这个过程不算困难。

        这篇文章主要记述了开发Godot协程时遇到的困难与经验总结。使用的语言是纯C#。

        Godot版本:4.3 mono

问题

什么是协程?

        简单来讲,协程是轻量级,用户级的线程,通过“分时复用” 的方式模拟多线程操作。

        协程是一个“伪线程”,它其实只在一个线程上执行,但是通过执行“挂起”和“恢复”等操作,可以实现“不同时间执行不同任务”或者说“一个任务分到多个帧内完成”。

        协程的优点就是缺点,缺点就是优点。它没有直接操作线程繁琐,也无法发挥多核处理器的优势,所以它并非“必要的”。

        不过对于游戏开发而言,游戏一般只有一个“主循环”,或者说一个主要线程,所以资源和数据(也可以说“上下文”(context))主要就只在一个线程中。所以一般情况我们是不会用到很多线程的,这时协程就是一个很好的选择,不必烦恼上下文同步等等多线程难题,可以直接拿来就用,还能不阻塞游戏主循环。

        理论上协程结合异步能发挥最大效果,因为协程中不能出现阻塞行为,异步可以有效避免阻塞,但是在写这篇文章时,未考虑到实现协程的异步。

Unity中的协程?

        “师夷长技以制夷”(doge),我们虽然要创建自己的协程系统,但好歹也给个学习参考方向吧,这不,Unity的协程为我们提供了一个很好的思路——迭代器实现协程。

        需要明确的是,这里的迭代器是指C#内置的迭代器,C#语言从语法层面上内置了很多种设计模式,最出名的应该就是“委托和事件”,它们隶属于“观察者模式”。而“迭代器模式”也被C#吃进肚子了。

        所以Unity的协程是基于C#的迭代器实现的,为什么是它?

迭代器与协程?

        先说结论,迭代器跟协程一点关系都没有。

        根据我们之前所说,协程的核心是通过执行“挂起”和“恢复”实现“分时复用”,所以只要能满足这个需求,用什么来实现协程都可以。

        那么Unity为什么选择了用迭代器的方式呢?我也不懂,毕竟人心不可测,我想可能是因为它有很多好处吧。

C#中的迭代器

        C#对“迭代器模式”提供“语法层面”上的支持,具体呢可以到官方文档了解,关键核心其实就是IEnumerator和yield。其实我觉得文档对这两者的关系什么的说得不是很明确,所以接下来我就按自己的思路去说吧:

        IEnumerator,枚举器接口,是实现迭代器的核心接口(所以它为什么不叫迭代器,比如什么IIterator之类的!!!!!!!)。接口中的成员就是实现迭代器功能所需要的成员,当我们实现一个迭代器后,可以手动设计迭代方式,比如“while循环遍历,满足条件就MoveNext”。

        或者用语法糖foreach,自动为我们生成完善的上述流程(foreach遍历本质上就是生成一套迭代器遍历流程的语法糖)。

        又或者可以使用yield关键字轻松实现迭代器功能,使用yield关键字的方法(或函数)就是“迭代器”方法(iterator methods)。yield有“产出,屈服,让步,弯曲”等意思,我个人偏向第一个意思,这样子就能比较好解释yield的功能,就是生出一个迭代出来的值。最好自己去看一下yield的用法,每次yield都会将当前执行的方法“挂起”,带着yield出来的值返回到调用者的执行流中。然后在下一次迭代时“恢复”到上一次yield语句之后继续执行,直到碰到下一个yield或yield break(用来终止迭代的)。

迭代器实现协程

        等等,是不是很熟悉?怎么又有“挂起”又有“恢复”的,难道说?!是的,我们可以通过使用迭代器方法快速实现协程,因为迭代器方法不仅能满足协程的需求,还便于使用,借由C#超级糖氏语法的支持,让协程的编写和调用易于实现(但是易不易于理解我就不好说了)。

        具体的思路就是,迭代器方法执行的是协程的“内容”,yield语句提供“挂起”和“恢复”功能,从而实现协程。至于迭代器方法,我个人简单的理解为“专用于迭代器的Lambda表达式”,结合上yield,它可以方便地创建出一个IEnumerator,而且类似一个“匿名的”IEnumerator。

设计协程系统

        思路倒是有了,但还是不会设计整个系统怎么办。那么看看Unity吧,我有段时间没用Unity了,所以可能记不清。不过大体思路还是有印象的,实在不行还能网上找找教程不是嘛。

        Unity中的协程系统是以单个MonoBehaviour类为执行单位的,也就是一般情况下只有MonoBehaviour类能使用协程,我也见过有些开发者是用一个继承MonoBehaviour的管理类单独控制所有协程的。

        与我而言,还是偏向于分划而治,也就是自己管理自己的协程(其实是因为这样就不用写一个全局管理类了);还有一点就是目前对Godot不够了解,所以不敢贸然乱写,以后有机会可以继续拓展。

        最终的结果就是:我用抽象了一个“协程”类,它的每个实例就代表一个协程。构造实例需要用到一个迭代器方法,也就是我们要执行的具体方法。我们可以通过读写类中的一些状态变量,控制协程的执行。

源码        

using System.Collections;namespace GoDogKit
{// Base class of CoroutineTaskpublic abstract class CoroutineTask{public abstract void Process(IEnumerator enumerator, double delta);public abstract bool IsDone(IEnumerator enumerator);}/// <summary>/// Used for stun a coroutine in a certain duration./// </summary>public class WaitForSeconds : CoroutineTask{private readonly double duration;private double currentTime;public WaitForSeconds(double duration){this.duration = duration;currentTime = 0;}public override void Process(IEnumerator enumerator, double delta) => currentTime += delta;public override bool IsDone(IEnumerator enumerator) => currentTime >= duration;}/// <summary>/// Represents a coroutine, used a IEnumerators to represent the coroutine's logic.    /// </summary>public class Coroutine{private readonly IEnumerator enumerator;private bool m_processable = false;private bool m_isDone = false;public Coroutine(IEnumerator enumerator, bool autoStart = true){this.enumerator = enumerator;m_processable = autoStart;}public Coroutine(IEnumerable enumerable, bool autoStart = true){enumerator = enumerable.GetEnumerator();m_processable = autoStart;}/// <summary>/// Make coroutine processable./// </summary>public virtual void Start(){m_processable = true;}/// <summary>/// What process do is actually enumerates all "yield return" in a "enumerator function" which /// constructs this coroutine. So you can put this function in any logics loop with/// a delta time parameter to update the coroutine's state in your preferred ways./// </summary>/// <param name="delta"> Coroutine process depends on the delta time. </param>public virtual void Process(double delta){// If coroutine is not started or already done, do nothingif (!m_processable || m_isDone) return;// If coroutine process encounted a CoroutineTask, process itif (enumerator.Current is CoroutineTask task){task.Process(enumerator, delta);if (task.IsDone(enumerator)){enumerator.MoveNext();}// Return if current task haven't donereturn;}// If there are no CoroutineTasks, just move to the next yield of the coroutine's enumeratorif (!enumerator.MoveNext()){m_processable = false;m_isDone = true;}}/// <summary>/// Pause the coroutine, also means it's not processable any more./// </summary>public virtual void Pause() => m_processable = false;/// <summary>/// Reset the coroutine, also means it's not done and not processable./// </summary>public virtual void Reset(){//TODO: iterator methods seems do not support Reset(), need to implement it manually            // enumerator.Reset();m_isDone = false;m_processable = false;}/// <summary>/// Stop the coroutine, also means it's done./// </summary>public virtual void Stop() => m_isDone = true;/// <summary>/// Get the enumerator which is used to construct this coroutine./// </summary>/// <returns> The enumerator of this coroutine. </returns>public IEnumerator GetEnumerator() => enumerator;/// <summary>/// Check if coroutine is done./// </summary>/// <returns> True if coroutine is done, otherwise false. </returns>public bool IsDone() => m_isDone;}
}

协程类 Coroutine         

        我们先看到底下的协程类,如上文思路所说的,这个类仅仅是对“迭代器方法” 的一个封装。

里面的核心就是那个IEnumerator成员变量,它寄存了我们迭代或者说我们协程执行的具体逻辑。

其他的变量都是些有的没的,用来记录状态。

        还有一个值得说明的点,就是虚方法Process(),在本套协程体系中,需要手动指定一个协程应该在哪个循环中进行处理,Unity貌似已经内置了在“Update”和“FixedUpdate”中都进行处理。

        但是我想把最大限度的对协程的控制权限交给使用者,所以就让使用者自己决定应该在哪里处理这个协程。真的不是因为太懒了,懒得写管理类,而且还不懂怎么插入Godot的主循环才放着不写的。

        协程的处理还是非常有必要的,这不是废话嘛!不处理协程我要它干嘛!只是为了提醒我自己记得用的时候把Process方法添上。

Reset()?

        这个是一个小问题,我本意是想用Reset()方法来实现单个协程类的复用,结果发现用迭代器方法实现的协程虽然自动实现了Current和MoveNext(),但好像并没有自动实现Reset()。

        调用的时候给了我一个NotSupportedException的报错。应该就是Reset没有实现的原因。

        后来因为可以通过new一个新的协程来使用,就懒得管能不能重用了,现在看来还是要研究一下比较好,万一要用到很多次协程的情况,老是new的话GC不得爆炸嘛。

        所以做了个TODO。

协程任务 CoroutineTask?

        前面也说了,通过yield语句实现“挂起”,具体意思就是“等待一段时间”,以实现分时复用。

yield return返回一个值到调用者,所以我们必须要在调用者范围内对该值进行处理,否则不就没有意义了。试想一下,处理协程的某个方法就是调用者,当yield return时,协程挂起,回到调用者,如果我们不对return回来的值进行任何处理,结果就是立刻继续处理协程,相当于yield return没有任何作用。

        拿Unity协程的“等待一段时间”为例,yield return返回一个用于指示等待时间的“值”,我们再处理协程的时候,如果发现yield return返回了一个这样的“值”,我们就应该对齐进行相应的处理,在这里即“等待一段时间”。

        在设计处理逻辑时,我突然发现这些逻辑之间始终存在某些共性,比如“进行单帧行为”,“判断是否能进入下一个逻辑”,于是乎就把它们抽象成CoroutineTask,意思是协程需要执行的任务。

        等待,抽象着抽象着我突然发现,所谓的CoroutineTask和Coroutine不是同一个东西吗,仔细看它们两者的成员方法,会发现惊天大共性。

        它们都有处理过程,判断过程。那么所谓的协程任务和协程实际上就是同一个东西啊。一旦我们接着继续抽象,就会发现:

嵌套协程?

        所以其实WaitForSeconds也可以是一个协程类,不过没有枚举器,因为它不用处理一段方法,只是简单的计时并等待一段时间,所以我们所有的逻辑都可以抽象成协程类,这样子除了减少类的复杂度,还有一个很重要的功能。

    // 修改WaitForSeconds,让其继承自Coroutine/// <summary>/// Used for stun a coroutine in a certain duration./// </summary>public class WaitForSeconds : Coroutine{private readonly double duration;private double currentTime;public WaitForSeconds(double duration) : base(enumerator: null, autoStart: true){this.duration = duration;currentTime = 0;}public override void Process(double delta) => currentTime += delta;public override bool IsDone() => currentTime >= duration;}

        就是可以实现协程之间的“循环嵌套”,试想一下,我们yield return时,不返回一个WaitForSeconds,而是new一个新的协程出来,而且它也能被正常处理。这样一来我们就实现了多个协程之间的嵌套执行。实现诸如“协程A可以等待协程B执行结束再执行“这样的效果。

        其实用CoroutineTask设计也能实现嵌套协程,无非就是给Coroutine多封装一层,让他变成CoroutineTask罢了。但我个人觉得但是类越少看得越舒服。

        这样就可以实现和Unity类似的功能了。那么我为什么还要留着CoroutineTask呢,除了不想浪费,还有一个重要的原因是CoroutineTask会比一个Coroutine更轻量,注意到WaitForSeconds类中其实没有用到枚举器变量,但为了实现继承我们还是将其继承了下来。

        所以抽象虽然好,但是抽得太多可能会有负面效果(“少抽点吧!”)。但是在这里,这个负面效果我觉得几乎没有影响,于是最后的最后我选择了第二种方案。

最终方案       

using System.Collections;namespace GoDogKit
{    /// <summary>/// Represents a coroutine, used a IEnumerators to represent the coroutine's logic.    /// </summary>public class Coroutine{private readonly IEnumerator enumerator;private bool m_processable = false;private bool m_isDone = false;public Coroutine(IEnumerator enumerator, bool autoStart = true){this.enumerator = enumerator;m_processable = autoStart;}public Coroutine(IEnumerable enumerable, bool autoStart = true){enumerator = enumerable.GetEnumerator();m_processable = autoStart;}/// <summary>/// Make coroutine processable./// </summary>public virtual void Start(){m_processable = true;}/// <summary>/// What process do is actually enumerates all "yield return" in a "enumerator function" which /// constructs this coroutine. So you can put this function in any logics loop with/// a delta time parameter to update the coroutine's state in your preferred ways./// </summary>/// <param name="delta"> Coroutine process depends on the delta time. </param>public virtual void Process(double delta){// If coroutine is not started or already done, do nothingif (!m_processable || m_isDone) return;// If coroutine process encounted a CoroutineTask, process itif (enumerator.Current is Coroutine coroutine){coroutine.Process(delta);if (coroutine.IsDone()){enumerator.MoveNext();}// Return if current task haven't donereturn;}// If there are no CoroutineTasks, just move to the next yield of the coroutine's enumeratorif (!enumerator.MoveNext()){m_processable = false;m_isDone = true;}}/// <summary>/// Pause the coroutine, also means it's not processable any more./// </summary>public virtual void Pause() => m_processable = false;/// <summary>/// Reset the coroutine, also means it's not done and not processable./// </summary>public virtual void Reset(){//TODO: iterator methods seems do not support Reset(), need to implement it manually            // enumerator.Reset();m_isDone = false;m_processable = false;}/// <summary>/// Stop the coroutine, also means it's done./// </summary>public virtual void Stop() => m_isDone = true;/// <summary>/// Get the enumerator which is used to construct this coroutine./// </summary>/// <returns> The enumerator of this coroutine. </returns>public virtual IEnumerator GetEnumerator() => enumerator;/// <summary>/// Check if coroutine is done./// </summary>/// <returns> True if coroutine is done, otherwise false. </returns>public virtual bool IsDone() => m_isDone;}/// <summary>/// Used for stun a coroutine in a certain duration./// </summary>public class WaitForSeconds : Coroutine{private readonly double duration;private double currentTime;public WaitForSeconds(double duration) : base(enumerator: null, autoStart: true){this.duration = duration;currentTime = 0;}public override void Process(double delta) => currentTime += delta;public override bool IsDone() => currentTime >= duration;}
}

        这就是最终方案了。

        我们可以愉快进行测试了。

测试

        上篇文章中,我们其实是为了实现一个摄像机震动功能,又不想用多线程,才迫不得已去实现自己的协程系统。

        那么回到初心,现在我们已经有了自己的协程系统。让我们根据需求定义一个迭代器方法,也就是我们的协程,用于实现震动效果。

        // Used for constructing the shake coroutine.private IEnumerator ShakeCoroutine(float duration, float magnitude, float frequency){float timer = 0.0f;while (timer < duration){timer += frequency;// 这个Extensions.RandomShpere是GodogKit的拓展// 其实就是在一个圆内随机出一个点GlobalPosition += Extensions.RandomShpere(magnitude);yield return new WaitForSeconds(frequency);}yield return null;}

        咋一看,这不就是Unity的协程嘛?!非常满意!终于可以在Godot里面写Unity了(doge)。

        然后在摄像机类中声明一个变量用于执行震动效果所用的协程:

private Coroutine m_shakeCoroutine = null;

         接着为协程的执行提供一个入口:

        /// <summary>/// Shake the camera./// </summary>/// <param name="duration"> The duration of the shake. </param>/// <param name="magnitude"> The magnitude of the shake. </param>/// <param name="frequency"> How often the camera should shake. </param>public void Shake(float duration, float magnitude, float frequency){m_shakeCoroutine = new Coroutine(ShakeCoroutine2(duration, magnitude, frequency));}

        这个方法暴露给外界以便调用。而实际上它进行的操作非常简单,仅仅只是为内置协程变量更换一个新的协程用于开启震动。

        最后重要的一点:放置处理函数!这里我选择放在_PhysicsProcess里。    

        public override void _PhysicsProcess(double delta){// If there is no target, do nothing.if (FollowTarget == null) return;switch (Behaviour){case BehaviourType.Normal: NormalFollow(delta); break;case BehaviourType.Inching: InchingFollow(delta); break;case BehaviourType.Slow: SlowFollow(delta); break;case BehaviourType.Predict: PredictFollow(delta); break;}// deal with shake coroutinem_shakeCoroutine?.Process(delta);}

        所以震动实现逻辑其实就是每次调用Shake的时候new一个新的协程给内置协程变量执行。

而且这里也不用什么判断语句,一旦新的协程被换上来,只要不为空,处理函数接着处理就行了。

如果你一直摁,那就一直new,然后一直处理。

        

        亲测之后没有任何卡顿。

        至于这样实现的性能方面,也就每次调用时消耗一个Coroutine类,无伤大雅。

        后面其实还有嵌套测试,我也自己试过了,能顺利执行。

        相关的具体代码以及最终方案应该都能在GoDogKit中找到。

MOWEIII/GoDogKit: A Plugin kit used by Godot which personally used and maybe continue to be update. (github.com)icon-default.png?t=O83Ahttps://github.com/MOWEIII/GoDogKit        这里我就不细讲了。

结语

        看着累,写着更累。

        没什么好多说的了,其实细细看来,这个协程系统不过三四十行代码,却能发挥很多效果。

        得益于C#的许多原生支持,才让协程的实现不那么困难。

        当然我们还得思路放开,不必拘泥于一招一式。

        还是继续学习和改进吧。

相关文章:

C# 游戏引擎中的协程

前言 书接上回&#xff0c;我谈到了Unity中的协程的重要性&#xff0c;虽然协程不是游戏开发“必要的”&#xff0c;但是它可以在很多地方发挥优势。 为了在Godot找回熟悉的Unity协程开发手感&#xff0c;不得不自己做一个协程系统&#xff0c;幸运的是&#xff0c;有了Unity的…...

如何封装微信小程序中的图片上传功能

文章目录 前言一、需求分析与设计思路二、上传图片功能封装三、页面调用示例四、功能改进与扩展4.1 压缩图片4.2 上传进度4.3 重试机制 五、总结 前言 在微信小程序开发中&#xff0c;图片上传功能是一个十分常见的需求&#xff0c;不管是社交分享、商城中的商品图片上传&…...

被问界/理想赶超!奔驰CEO再度“出马”,寻找中国外援

来自中国车企的全方位、持续施压&#xff0c;让大部分外资车企开始寻求更多的本地化合作来实现技术升级。传统豪华品牌也同样如此。 本周&#xff0c;知情人士透露&#xff0c;梅赛德斯奔驰首席执行官Ola Kllenius计划再次访问中国&#xff0c;目的是进一步寻求和扩大与本地技术…...

魔改xjar支持springboot3,

jar包加密方案xjar, 不支持springboot3。这个发个魔改文章希望大家支持 最近公司需要将项目部署在第三方服务器&#xff0c;于是就有了jar包加密的需求&#xff0c;了解了下目前加密方案现况如下: 混淆方案&#xff0c;就是在代码中添加大量伪代码&#xff0c;以便隐藏业务代…...

python json文件读写

在Python中处理JSON文件是一个常见的任务。JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写&#xff0c;同时也易于机器解析和生成。Python提供了内置的json模块来帮助我们读取和写入JSON格式的数据。 如何读…...

Android常用C++特性之std::find_if

声明&#xff1a;本文内容生成自ChatGPT&#xff0c;目的是为方便大家了解学习作为引用到作者的其他文章中。 std::find_if 是 C 标准库中的一个算法&#xff0c;用于在给定范围内查找第一个满足特定条件的元素。它接受一个范围&#xff08;由迭代器指定&#xff09;和一个谓词…...

19 vue3之自定义指令Directive按钮鉴权

directive-自定义指令(属于破坏性更新) Vue中有v-if,v-for,v-bind,v-show,v-model 等等一系列方便快捷的指令 今天一起来了解一下vue里提供的自定义指令 Vue3指令的钩子函数 created 元素初始化的时候beforeMount 指令绑定到元素后调用 只调用一次mounted 元素插入父级dom…...

数据资产新范式,URP城市焕新平台东博会首发!

城市数据资产蕴藏着巨大的宝藏。今年1月&#xff0c;国家数据局印发《“数据要素”三年行动计划&#xff08;2024—2026年&#xff09;》&#xff0c;将“数据要素智慧城市”上升为“数据要素”计划的重要部分&#xff0c;加速释放城市数据资产价值。 高质量发展以数据要素驱动…...

儿童乐园软件下载安装 佳易王游乐场会员扣次管理系统操作教程

一、前言 儿童乐园软件下载安装 佳易王游乐场会员扣次管理系统操作教程 软件为绿色免安装版&#xff0c;已经内置数据库&#xff0c;不需再安装数据库文件&#xff0c;软件解压即可。 二、软件程序教程 1、软件可同时管理多个项目&#xff0c;项目设置方法如图&#xff0c;点…...

windows下 Winobj.exe工具使用说明c++

1、winobj.exe工具下载地址 WinObj - Sysinternals | Microsoft Learn 2、接下来用winobj.exe查看全局互斥&#xff0c;先写一个小例子 #include <iostream> #include <stdlib.h> #include <tchar.h> #include <string> #include <windows.h>…...

提示词工程 (Prompt Engineering) 最佳实践

prompt Engineering 概念解析 提示工程是一门较新的学科&#xff0c;关注提示词开发和优化&#xff0c;帮助用户将大语言模型&#xff08;Large Language Model, LLM&#xff09;用于各场景和研究领域。研究人员可利用提示工程来提升大语言模型处理复杂任务场景的能力&#xf…...

【读写分离?聊聊Mysql多数据源实现读写分离的几种方案】

文章目录 一.什么是MySQL 读写分离二.读写分离的几种实现方式(手动控制)1.基于Spring下的AbstractRoutingDataSource1.yml2.Controller3.Service实现4.Mapper层5.定义多数据源6.继承Spring的抽象路由数据源抽象类&#xff0c;重写相关逻辑7. 自定义注解WR&#xff0c;用于指定当…...

C++游戏

宠粉福利&#xff01; 目录 1.猜数字 2.五子棋 3.打怪 4.跑酷 5.打飞机 6.扫雷 1.猜数字 #include <iostream> #include <cstdlib> #include <ctime>int main() {std::srand(static_cast<unsigned int>(std::time(0))); // 设置随机数种子int …...

探索顶级低代码开发平台,实现创新

文章盘点ZohoCreator、OutSystems等10款顶尖低代码开发平台&#xff0c;各平台以快速开发、集成、数据安全等为主要特点&#xff0c;适用于不同企业需求&#xff0c;助力数字化转型。 一、Zoho Creator Zoho Creator 是一个低代码开发平台&#xff0c;它简化了应用开发中的复杂…...

Html--笔记01:使用软件vscode,简介Html5--基础骨架以及标题、段落、图片标签的使用

一.使用VSC--全称&#xff1a;Visual Studio Code vscode用来写html文件&#xff0c;打开文件夹与创建文件夹&#xff1a;①选择文件夹 ②拖拽文件 生成浏览器的html文件的快捷方式&#xff1a; &#xff01;enter 运行代码到网页的方法&#xff1a; 普通方法&#xff1a…...

探索反向传播:深度学习中优化神经网络的秘密武器

反向传播的概念&#xff1a; 反向传播&#xff08;Backpropagation&#xff09; 是深度学习中训练神经网络的核心算法。它通过有效计算损失函数相对于模型参数的梯度&#xff0c;使得模型能够通过梯度下降等优化方法逐步调整参数&#xff0c;从而最小化损失函数&#xff0c;提…...

K8S精进之路-控制器DaemonSet -(3)

介绍 DaemonSet就是让一个节点上只能运行一个Daemonset Pod应用&#xff0c;每个节点就只有一个。比如最常用的网络组件&#xff0c;存储插件&#xff0c;日志插件&#xff0c;监控插件就是这种类型的pod.如果集群中有新的节点加入&#xff0c;DaemonSet也会在新的节点创建出来…...

【JVM】类加载机制

文章目录 类加载机制类加载过程1. 加载2. 验证3. 准备4. 解析偏移量符号引用和直接引用 5. 初始化 类加载机制 类加载指的是&#xff0c;Java 进程运行的时候&#xff0c;需要把 .class 文件从硬盘读取到内存&#xff0c;并进行一些列的校验解析的过程&#xff08;程序要想执行…...

ENV | 5步安装 npm node(homebrew 简洁版)

1. 操作步骤 1.1 安装 homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"1.2 安装 node # 安装最新版 brew install node # 安装指定版本&#xff0c;如18 brew install node181.3 安装 nvm&#xff08…...

EasyExcel全面实战:掌握多样化的Excel导出能力

1 概述 本文将通过实战案例全面介绍EasyExcel在Excel导出方面的多种功能。内容涵盖多表头写入、自定义格式、动态表头生成、单元格合并应用等。通过这些实例,读者可以掌握EasyExcel的各种高级功能,并在实际项目中灵活应用。 白日依山尽,黄河入海流。 欲穷千里目,更上一层楼…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动

一、前言说明 在2011版本的gb28181协议中&#xff0c;拉取视频流只要求udp方式&#xff0c;从2016开始要求新增支持tcp被动和tcp主动两种方式&#xff0c;udp理论上会丢包的&#xff0c;所以实际使用过程可能会出现画面花屏的情况&#xff0c;而tcp肯定不丢包&#xff0c;起码…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

EtherNet/IP转DeviceNet协议网关详解

一&#xff0c;设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络&#xff0c;本网关连接到EtherNet/IP总线中做为从站使用&#xff0c;连接到DeviceNet总线中做为从站使用。 在自动…...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包

文章目录 现象&#xff1a;mysql已经安装&#xff0c;但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时&#xff0c;可能是因为以下几个原因&#xff1a;1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

计算机基础知识解析:从应用到架构的全面拆解

目录 前言 1、 计算机的应用领域&#xff1a;无处不在的数字助手 2、 计算机的进化史&#xff1a;从算盘到量子计算 3、计算机的分类&#xff1a;不止 “台式机和笔记本” 4、计算机的组件&#xff1a;硬件与软件的协同 4.1 硬件&#xff1a;五大核心部件 4.2 软件&#…...

Golang——9、反射和文件操作

反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一&#xff1a;使用Read()读取文件2.3、方式二&#xff1a;bufio读取文件2.4、方式三&#xff1a;os.ReadFile读取2.5、写…...

【 java 虚拟机知识 第一篇 】

目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...