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 CoroutineTask
    public 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 nothing
            if (!m_processable || m_isDone) return;

            // If coroutine process encounted a CoroutineTask, process it
            if (enumerator.Current is CoroutineTask task)
            {
                task.Process(enumerator, delta);

                if (task.IsDone(enumerator))
                {
                    enumerator.MoveNext();
                }

                // Return if current task haven't done
                return;
            }

            // If there are no CoroutineTasks, just move to the next yield of the coroutine's enumerator
            if (!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 nothing
            if (!m_processable || m_isDone) return;

            // If coroutine process encounted a CoroutineTask, process it
            if (enumerator.Current is Coroutine coroutine)
            {
                coroutine.Process(delta);

                if (coroutine.IsDone())
                {
                    enumerator.MoveNext();
                }

                // Return if current task haven't done
                return;
            }

            // If there are no CoroutineTasks, just move to the next yield of the coroutine's enumerator
            if (!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 coroutine
            m_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#的许多原生支持,才让协程的实现不那么困难。

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

        还是继续学习和改进吧。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

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

PK过Google、Facebook,YouTube竟然是外贸引流营销的新前景

在如今的外贸行业中&#xff0c;广告投放已经成为商家吸引客户和提高销量的重要工具。众所周知&#xff0c;Facebook和谷歌是广告投放的两大巨头平台。这两者以其强大的用户基数和广告精准性在市场上占据主导地位。然而&#xff0c;随着互联网的发展和消费趋势的改变&#xff0…

MongoDB 工具包安装(mongodb-database-tools)

首先到官网下载工具包&#xff0c;进入下面页面&#xff0c;复制连接地址&#xff0c;使用wget下载 cd /usr/local/mongodb5.0.14/wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-rhel70-x86_64-100.6.1.tgz 安装 tar -zxvf mongodb-database-tools-rhel70-…

《北方牧业》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《中国动物检疫》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《中国动物检疫》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a;河北省畜牧局 主办单…

【rust】 基于rust编写wasm,实现markdown转换为html文本

文章目录 背景转换预览核心代码前置依赖rustup换源 cargo本地路径修改&#xff08;可选&#xff09;cargo换源中科大 wasm-pack安装 背景 尝试用rust编写一款markdown转html的插件&#xff0c;通过wasm给html使用&#xff0c;不得不说体积挺小&#xff0c;约200K&#xff0c; …

spring cloud Gateway网关

网关是将所有面向用户的服务接口统一管理的代理服务器&#xff0c;所有内部服务的远程调用都是在局域网内部&#xff0c;而网关是在公网中。 一、依赖 通过访问网关调用项目中的服务&#xff0c;需要使用Eureka&#xff0c;网关服务器需要在Eureka服务注册它自己&#xff0c;本…

Clipboard.js实现复制文本到剪贴板功能

一、Clipboard.js简介 Clipboard.js是一个轻量级的实现复制文本到剪贴板功能的JavaScript插件&#xff0c;该插件可以将输入框&#xff0c;文本域&#xff0c;DOM节点元素中的文本内容复制到剪贴板中。 官网地址&#xff1a;Clipboard.js 浏览器兼容性&#xff1a;兼容Chrome、…

Ubuntu环境切换到服务器某个用户后source等命令和Tab快捷补全都用不了了,提示没找到,但root用户可以

以escs用户为例&#xff1a; 输入以下命令 grep root /etc/passwd grep escs /etc/passwd 对比发现&#xff0c;root用户配的是bash&#xff0c;而escs却是sh&#xff0c; 所以把escs的sh改成和root一样的bash&#xff0c;命令为 usermod -s /bin/bash escs 改好后就可以了。 …

VSCODE驯服日记(三):配置C++环境

1. 下载mingw64&#xff0c;解压后把bin并添加到环境变量 1>编译器介绍 mingw&#xff1a;专为windowsgcc&#xff1a;多平台msvc &#xff1a;windows&#xff0c;且配合vs使用更佳 注意与调试器gdb和lldb的区别 2. 安装vscode插件&#xff1a; 安装C/C插件 安装code ru…

测试管理新增视图与高级搜索功能,测试计划支持一键生成缺陷详情,MeterSphere开源持续测试工具v3.3版本发布

2024年9月29日&#xff0c;MeterSphere开源持续测试工具正式发布v3.3版本。 在这一版本中&#xff0c;接口测试方面&#xff0c;接口导入功能支持导入Postman、JMX、HAR和MeterSphere格式的文件&#xff0c;接口场景的自定义请求步骤支持cURL快捷导入&#xff1b;测试管理方面…

基于趋近律的滑模控制器设计、仿真(S-function)

目录 一、什么是滑模控制?1. 滑模面2. 控制策略3. 抗干扰和鲁棒性4. 滑模控制的应用 二、什么是趋近律&#xff1f;1. 趋近律三大设计目标2. 常见的趋近律形式1. 等速趋近律2. 指数趋近律3. 幂次趋近律 三、滑模控制器设计四、滑模仿真示例1. Simulink仿真框图2. 不同k值下的仿…

MAC如何获取文件数字签名和进程名称

1、安装需要查看数字签名和进程名称的软件包 2、打开终端命令行&#xff08;Terminal&#xff09; 3、查找数字签名 在终端命令行中输入&#xff1a; codesign -dvv 安装的软件路径 2>&1 | grep "Authority" | head -n 1 | cut -d -f2”回显即为进程的数…

kafka发送消费核心参数与设计原理详解

核心参数详解: 发送端参数: 发送方式:默认值一般都是1: 重试参数 : 批量参数: 消费端参数: 自动提交参数: 如果是false,就是说消费完后不提交位移。也就是说比如之前消费的1-5.第N次还是消费到1-5.如果是false。那么第一次消费1-3,第二次消费4-5:默认是true:我们…

HCIP和HCIE有什么区别呢?

HCIP和HCIE有什么区别呢&#xff1f;今天给大家介绍下两者的不同 ‌认证层次‌&#xff1a;HCIE屹立于华为认证体系的顶端&#xff0c;定位为专家级认证&#xff1b;而HCIP则位于中坚位置&#xff0c;属于中级认证。 难度与专业要求‌&#xff1a;通往HCIE之路布满挑战&…

机器学习实战26-一种基于LightGBM的股市涨跌预测系统与代码实现过程

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下机器学习实战26-一种基于LightGBM的股市涨跌预测系统与代码实现过程。文章首先阐述了项目背景&#xff0c;随后详细解释了LightGBM模型的原理及其在股市预测中的应用。通过选取具有代表性的样例股票数据集&#xff…

深入浅出MongoDB(三)

深入浅出MongoDB&#xff08;三&#xff09; 文章目录 深入浅出MongoDB&#xff08;三&#xff09;复制副本集设置分片分片实例备份与恢复监控ObjectId 复制 复制时将数据同步在多个服务器的过程&#xff0c;提供了数据的冗余备份&#xff0c;在多个服务器上存储数据副本&#…

视频压缩怎么压缩更小?分享释放视频占用空间的小技巧

想问问同样在做视频自媒体博主的你&#xff0c;都是怎么解决占满内存的视频素材的&#xff1f; 是有听说有一部分博主会把录制好的&#xff0c;还未使用的视频素材&#xff0c;用工具压缩他们的体积&#xff0c;以减少占用内存空间。 小编也有试过&#xff0c;发现视频的体积是…

oracle解决关联查询报invalid number问题

出现问题的原因和背景 oracle进行关联查询的时候因为字段存在多个用逗号切割的id&#xff0c;导致查询的过程中报无效数字或非法数字 问题复现 新建表A CREATE TABLE "A" (id NUMBER NOT NULL,name VARCHAR2(255 BYTE) )INSERT INTO "A" VALUES (1, 上海…

【Spring】lombok、dbUtil插件应用

一、lombok插件 1. 功能&#xff1a;对实体类自动&#xff0c;动态生成get、set方法&#xff0c;无参、有参构造..... 2. 步骤&#xff1a; &#xff08;1&#xff09;idea安装插件(只做一次) &#xff08;2&#xff09;添加坐标 &#xff08;3&#xff09;编写注解 NoArgsCo…

Codeforces Round 975 (Div. 2) A-C 题解

这次看到 C 题分数 1750 就开始害怕了&#xff0c;用小号打的比赛&#xff0c;一直觉得做不出来&#xff0c;最后才想到 A. Max Plus Size 题意 给你一些整数&#xff0c;选择一些涂成红色&#xff0c;两两不能相邻&#xff0c;你的得分为&#xff1a; [ 红色元素的个数 ] …