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

UE 多线程

 详细参考:《Exploring in UE4》多线程机制详解[原理分析] - 知乎 (zhihu.com)

UE4 C++基础 - 多线程 - 知乎 (zhihu.com) 

多线程的好处

  • 通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程模式,同步编程模式要比异步编程模式简单得多。
  • 多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享。而多个线程自动地可以访问相同的储存地址空间和文件描述符。
  • 有些问题可以通过将其分解从而改善整个程序的吞吐量。在只有一个控制线程的情况下,单个进程需要完成多个任务时,实际上需要把这些任务串行化;有了多个控制线程,相互独立的任务的处理就可以交叉进行,只需要为每个任务分配一个单独的线程,当然只有在处理过程互不依赖的情况下,两个任务的执行才可以穿插进行。
  • 交互的程序同样可以通过使用多线程实现响应时间的改善,多线程可以把程序中处理用户输入输出的部分与其他部分分开。

 UE4中的多线程

FRunnable

标准模板

//Runnable.h
class CORE_API FRunnable
{
public:virtual bool Init(){return true;}virtual uint32 Run() = 0;virtual void Stop() { }virtual void Exit() { }virtual class FSingleThreadRunnable* GetSingleThreadInterface( ){return nullptr;}virtual ~FRunnable() { }
};

        实际上,在实现多线程的时候,我们需要将FRunnable作为参数传递到真正的线程里面,然后才能通过线程去调用FRunnable的Run,也就是我们具体实现的类的Run方法(通过虚函数覆盖父类的Run)。所谓真正的线程其实就是FRunnableThread,不同平台的线程都继承自他 

或者

        UE4是跨平台的引擎,对各个平台线程实现进行了封装,抽象出了 FRunnable 。引擎中大部分的需要多线程执行逻辑都是继承这个类实现的多线程 

#include "HAL/Runnable.h"class MyRunnable : public FRunnable {
public:virtual bool Init() override;  // 初始化 runnable 对象virtual uint32 Run() override; // 运行 runnable 对象virtual void Stop() override;  // 停止 runnable 对象,线程提前终止时被调用virtual void Exit() override;  // 退出 runnable 对象
};bool MyRunnable::Init() { return true; }
uint32 MyRunnable::Run() { return 0; }
void MyRunnable::Stop() {}
void MyRunnable::Exit() {} 

        调用顺序是 Init()Run()Exit()。Runnable对象初始化操作在 Init() 函数中完成,并通过返回值确定是否成功。初始化失败,则该线程停止执行,并返回一个错误码;成功,则会执行 Run() ;执行完毕后,则会调用 Exit() 执行清理操作。 

FRunnableThread 

        Runnable负责具体业务逻辑的执行,UE4中使用 FRunnableThread 表示一个可执行的线程。 可以通过调用 FRunnableThread::Create 完成线程的创建:

#include "HAL/RunnableThread.h"static FRunnableThread * Create
(class FRunnable * InRunnable, // Runnable 对象const TCHAR * ThreadName,     // 线程名称uint32 InStackSize,           // 线程栈大小,0表示使用当前线程的栈大小EThreadPriority InThreadPri,  // 线程优先级uint64 InThreadAffinityMask
);// 返回值:若成功则返回创建的线程,否则返回 nullptr

样例代码如下:

#include "HAL/RunnableThread.h"FRunnable * Runnable = new MyRunnable();
FRunnableThread* RunnableThread = FRunnableThread::Create(Runnable, TEXT("LaLaLaDeMaXiYa!"));

线程对象创建成功后即开始执行Runnable对象的 Init () 函数,如果成功则分别执行Run() 和 Exit() 函数。

线程标识

        每个线程都有一个线程ID,线程ID在它所属的进程环境中有效。为增加标识性,UE4还增加了线程名称。线程ID是唯一的,线程名称可以重复。可通过GetThreadID 和 GetThreadName 获取线程ID和名称。

const uint32 GetThreadID() const;
const FString & GetThreadName() const;

线程终止 

单个线程可以通过如下三种方式退出。

  1. 线程执行完 runnable 对象的 Run() 和 Exit() 函数后正常退出
  2. 调用 WaitForCompletion() 函数,阻塞调用例程直到线程执行完毕
  3. 调用 Kill(bool bShouldWait=false) 函数,会先执行 runnable 对象的 stop 函数,然后根据 bShouldWait 参数决定是否等待线程执行完毕。如果不等待,则强制杀死线程,可能会造成内存泄漏。
void WaitForCompletion(); // 阻塞调用例程,直到线程执行完毕
bool Kill(bool bShouldWait); // 强制杀掉线程

FThreadManager 

        通过FRunnableThread 创建的线程是通过 FThreadManager 进行统一管理。

// ThreadingBase.cpp FRunnableThread::Create 函数
// Call the thread's create method
if (NewThread->CreateInternal(InRunnable,ThreadName,InStackSize,InThreadPri,InThreadAffinityMask) == false)

        CreateInternal根据平台的不同实现不同,常用平台中,Android和iOS都是采用的 pthread标准线程库,Windows平台是单独实现的。线程创建完毕后会统一调用

FThreadManager::Get().AddThread(ThreadID, this);

        将线程本身添加至管理器。如 WindowsRunnableThread.h         FRunnableThreadWin::CreateInternal 函数。标准线程对象 FRunnableThreadPThread 则是在入口点:

virtual PthreadEntryPoint GetThreadEntryPoint() {return _ThreadProc;
}static void *STDCALL _ThreadProc(void *pThis) {check(pThis);FRunnableThreadPThread* ThisThread = (FRunnableThreadPThread*)pThis;// cache the thread ID for this thread (defined by the platform)ThisThread->ThreadID = FPlatformTLS::GetCurrentThreadId();// ====================>>这里将线程本身加入管理器 <<==========================FThreadManager::Get().AddThread(ThisThread->ThreadID, ThisThread);// set the affinity.  This function sets affinity on the current thread, so don't call in the Create function which will trash the main thread affinity.FPlatformProcess::SetThreadAffinityMask(ThisThread->ThreadAffinityMask);		// run the thread!ThisThread->PreRun();ThisThread->Run();ThisThread->PostRun();pthread_exit(NULL);return NULL;
}

线程池 

        线程过多会带来调度开销,进而影响缓存局部性和整体性能。频繁创建和销毁线程也会带来极大的开销。通常我们更加关心的是任务可以并发执行,并不想管理线程的创建,销毁和调度。通过将任务处理成队列,交由线程池统一执行,可以提升任务的执行效率。UE4提供了对应的线程池来满足我们的需求。异步任务统一都继承至 IQueuedWork,属于抽象接口类,可供我们直接使用的是

  • FAsyncTask 异步任务,自动加入线程池
  • FAutoDeleteAsyncTask 异步任务,任务完成后会自动销毁

        异步任务通常继承 FNonAbandonableTask,表明该任务不可被抛弃,必须被执行完毕。样例代码如下:

idi#include "Async/AsyncWork.h"class ExampleAsyncTask : public FNonAbandonableTask
{friend class FAsyncTask<ExampleAsyncTask>;friend class FAutoDeleteAsyncTask<ExampleAsyncTask>;int32 ExampleData;ExampleAsyncTask(int32 InExampleData): ExampleData(InExampleData){}void DoWork() {UE_LOG(LogBlankProgram, Display, TEXT("ExampleAsyncTask %d Work."), ExampleData);}FORCEINLINE TStatId GetStatId() const {RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);}
};void Example {// 2.1 线程池异步队列FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>(1);// 交由后台控制任务开始执行时机MyTask->StartBackgroundTask();// 确保线程被执行完成MyTask->EnsureCompletion();delete MyTask;
}

 AsyncTask系统

        AsyncTask系统是一套基于线程池的异步任务处理系统,样例如下:

  //AsyncWork.hclass ExampleAsyncTask : public FNonAbandonableTask{friend class FAsyncTask<ExampleAsyncTask>;int32 ExampleData;ExampleAsyncTask(int32 InExampleData): ExampleData(InExampleData){}void DoWork(){... do the work here}FORCEINLINE TStatId GetStatId() const{RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);}};void Example(){//start an example jobFAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>( 5 );MyTask->StartBackgroundTask();//--or --MyTask->StartSynchronousTask();//to just do it now on this thread//Check if the task is done :if (MyTask->IsDone()){}//Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.//Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.MyTask->EnsureCompletion();delete Task;}
FQueuedThreadPool线程池 

        FQueuedThreadPool,和一般的线程池实现类似,线程池里面维护了多个线程FQueuedThread与多个任务队列IQueuedWork,线程是按照队列的方式来排列的。在引擎PreInit的时候执行相关的初始化操作,代码如下

// FEngineLoop.PreInit   LaunchEngineLoop.cpp
if (FPlatformProcess::SupportsMultithreading())
{{GThreadPool = FQueuedThreadPool::Allocate();int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();// we are only going to give dedicated servers one pool threadif (FPlatformProperties::IsServerOnly()){NumThreadsInThreadPool = 1;}verify(GThreadPool->Create(NumThreadsInThreadPool, 128 * 1024));}
#ifUSE_NEW_ASYNC_IO{GIOThreadPool = FQueuedThreadPool::Allocate();int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();if (FPlatformProperties::IsServerOnly()){NumThreadsInThreadPool = 2;}verify(GIOThreadPool->Create(NumThreadsInThreadPool, 16 * 1024, TPri_AboveNormal));}
#endif// USE_NEW_ASYNC_IO#ifWITH_EDITOR// when we are in the editor we like to do things like build lighting and such// this thread pool can be used for those purposesGLargeThreadPool = FQueuedThreadPool::Allocate();int32 NumThreadsInLargeThreadPool = FMath::Max(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2, 2);verify(GLargeThreadPool->Create(NumThreadsInLargeThreadPool, 128 * 1024));
#endif
}

        专有服务器的线程池GThreadPool默认只开一个线程,非专有服务器的根据核数开(CoreNum-1)个线程。编辑器模式会另外再创建一个线程池GLargeThreadPool,包含(LogicalCoreNum-2)个线程,用来处理贴图的压缩和编码相关内容。

        在线程池里面所有的线程都是FQueuedThread类型,不过更确切的说FQueuedThread是继承自FRunnable的线程执行体,每个FQueuedThread里面包含一个FRunnableThread作为内部成员。

        相比一般的线程,FQueuedThread里面多了一个成员FEvent* DoWorkEvent,也就是说FQueuedThread里面是有一个事件触发机制的。那么这个事件机制的作用是什么?一般情况下来说,就是在没有任务的时候挂起这个线程,在添加并分配给该线程任务的时候激活他,不过你可以灵活运用它,在你需要的时候去动态控制线程任务的执行与暂停。前面我们在给线程池初始化的时候,通过FQueuedThreadPool的Create函数创建了多个FQueuedThread,然后每个FQueuedThread会执行Run函数,里面有一段逻辑如下:

//ThreadingBase.cpp
bool bContinueWaiting = true;
while(bContinueWaiting )
{				DECLARE_SCOPE_CYCLE_COUNTER(TEXT( "FQueuedThread::Run.WaitForWork" ), STAT_FQueuedThread_Run_WaitForWork, STATGROUP_ThreadPoolAsyncTasks );// Wait for some work to dobContinueWaiting = !DoWorkEvent->Wait( 10 );
}
//windows平台下的wait
bool FEventWin::Wait(uint32 WaitTime, const bool bIgnoreThreadIdleStats/*= false*/)
{WaitForStats();SCOPE_CYCLE_COUNTER(STAT_EventWait );check(Event );FThreadIdleStats::FScopeIdleScope(bIgnoreThreadIdleStats );return (WaitForSingleObject( Event, WaitTime ) == WAIT_OBJECT_0);
}

        我们看到,当DoWorkEvent执行Wait的时候,如果该线程的Event处于无信号状态(默认刚创建是无信号的),那么wait会等待10毫秒并返回false,线程处于While无限循环中。如果线程池添加了任务(AddQueuedWork)并执行了DoWorkEvent的Trigger函数,那么Event就会被设置为有信号,Wait函数就会返回true,随后线程跳出循环进而处理任务。

注:FQueuedThread里的DoWorkEvent是通FPlatformProcess::GetSynchEventFromPool();从EventPool里面获取的。WaitForSingleObject等内容涉及到Windows下的事件机制,大家可以自行到网上搜索相关的使用,这里给出一个官方的 使用案例。
 Asyntask与IQueuedWork

        线程池的任务IQueuedWork本身是一个接口,所以得有具体实现。这里你就应该能猜到,所谓的AsynTask其实就是对IQueuedWork的具体实现。这里AsynTask泛指FAsyncTask与FAutoDeleteAsyncTask两个类,我们先从FAsyncTask说起。

FAsyncTask有几个特点,

  1. FAsyncTask是一个模板类,真正的AsyncTask需要你自己写。通过DoWork提供你要执行的具体任务,然后把你的类作为模板参数传过去
  2. 使用FAsyncTask就默认你要使用UE提供的线程池FQueuedThreadPool,前面代码里说明了在引擎PreInit的时候会初始化线程池并返回一个指针GThreadPool。在执行FAsyncTask任务时,如果你在执行StartBackgroundTask的时候会默认使用GThreadPool线程池,当然你也可以在参数里面指定自己创建的线程池
  3. 创建FAsyncTask并不一定要使用新的线程,你可以调用函数StartSynchronousTask直接在当前线程上执行任务
  4. FAsyncTask本身包含一个DoneEvent,任务执行完成的时候会激活该事件。当你想等待一个任务完成时再做其他操作,就可以调用EnsureCompletion函数,他可以从队列里面取出来还没被执行的任务放到当前线程来做,也可以挂起当前线程等待DoneEvent激活后再往下执行

FAutoDeleteAsyncTask与FAsyncTask是相似的,但是有一些差异,

  1. 默认使用UE提供的线程池FQueuedThreadPool,可以通过参数指定使用其他线程池
  2. FAutoDeleteAsyncTask在任务完成后会通过线程池的Destroy函数删除自身或者在执行DoWork后删除自身,而FAsyncTask需要手动delete
  3. 包含FAsyncTask的特点1和特点3

总的来说,AsyncTask系统实现的多线程与你自己字节继承FRunnable实现的原理相似,不过他在用法上比较简单,而且还可以直接借用UE4提供的线程池,很方便。

线程同步

        当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取或者修改的,那么就不存在一致性问题。同样地,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。这个时候就需要用到线程同步机制。

UE4提供了以下几个不同类别的同步机制:

Atomics 原子机制

        Atomic operations(原子操作) 保证CPU在读取和写入内存时总线操作是不可分割的。它是许多高级同步机制的基础,主要优势是可以进行比较快的进行比较和解锁操作。一个用Atomics实现的样例如下:

class FThreadSafeCounter{
public:
int32 Add( int32 Amount ) {return FPlatformAtomics::InterlockedAdd(&Counter, Amount);}
private:volatile int32 Counter; // 因为值可能以编译器无法预测的异步方式被改变,声明为volatile禁用优化
};

 Locking 锁机制

        在UE4中常用的两种锁机制是 Critical Sections(临界区)和 SpinLocks(自旋锁)。

  • FSpinLock 自旋锁
  • FScopeLock区域锁
  • FCriticalSection 临界区
  • FRWLock 读写锁

 Signaling 信号机制

  • FSemaphore信号量与互斥锁类型,但是他包含了一种信号机制。缺点是不是所有平台都支持。更加常用的线程间通信机制是 FEvent

 Waiting

  • FEvent事件
    • 阻塞直至被触发或者超时
    • 经常被用来激活其他工作线程
  • FScopedEvent区域事件
    • 对FEvent的一次包装,阻塞在域代码退出时
{FScopedEvent MyEvent;SendReferenceOrPointerToSomeOtherThread(&MyEvent); // Other thread calls MyEvent->Trigger() ;// MyEvent destructor is here, we wait here.
}

          其中 FCriticalSection 是根据各个平台的互斥锁进行的抽象。Windows 平台是基于Windows平台的临界区。常用的iOS, Android,Linux平台则是使用的POSIX的线程标准实现[13]。

其他        

        UE4常见的容器类【TArray, TMap, TSet】通常都不是线程安全的,需要我们仔细编写代码保证线程安全。下面是几个常见的线程安全类:

  • FThreadSafeCounter​​​​​​​计数器
  • FThreadSingleton 单例类
  • FThreadIdleStats 线程空闲状态统计类
  • TLockFreePointerList 无锁队列
  • TQueue队列

        下面是一个简单的线程安全TSet,附带FCriticalSection使用示例。

/** Simple thread safe proxy for TSet<FName> */
template <typename T>
class FThreadSafeSet
{TSet<T> InnerSet;FCriticalSection SetCritical;
public:void Add(T InValue) {FScopeLock SetLock(&SetCritical);InnerSet.Add(InValue);}bool AddUnique(T InValue) {FScopeLock SetLock(&SetCritical);if (!InnerSet.Contains(InValue)){InnerSet.Add(InValue);return true;}return false;}bool Contains(T InValue) {FScopeLock SetLock(&SetCritical);return InnerSet.Contains(InValue);}void Remove(T InValue) {FScopeLock SetLock(&SetCritical);InnerSet.Remove(InValue);}void Empty() {FScopeLock SetLock(&SetCritical);InnerSet.Empty();}void GetValues(TSet<T>& OutSet) {FScopeLock SetLock(&SetCritical);OutSet.Append(InnerSet);}int32 Num() { return InnerSet.Num();}
};

完整代码

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "BlankProgram.h"
#include "RequiredProgramMainCPPInclude.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "Async/AsyncWork.h"DEFINE_LOG_CATEGORY_STATIC(LogBlankProgram, Log, All);
IMPLEMENT_APPLICATION(BlankProgram, "BlankProgram");class MyRunnable : public FRunnable {
public:virtual bool Init() override;  // 初始化 runnable 对象virtual uint32 Run() override; // 运行 runnable 对象virtual void Stop() override;  // 停止 runnable 对象,线程提前终止时被调用virtual void Exit() override;  // 退出 runnable 对象
};bool MyRunnable::Init() {UE_LOG(LogBlankProgram, Display, TEXT("Thread Init."));return true;
}uint32 MyRunnable::Run() {UE_LOG(LogBlankProgram, Display, TEXT("Thread Run."));return 0;
}void MyRunnable::Stop() {}void MyRunnable::Exit() {UE_LOG(LogBlankProgram, Display, TEXT("Thread Exit."));
}// 任务队列
class ExampleAsyncTask : public FNonAbandonableTask {friend class FAsyncTask<ExampleAsyncTask>;friend class FAutoDeleteAsyncTask<ExampleAsyncTask>;int32 ExampleData;ExampleAsyncTask(int32 InExampleData): ExampleData(InExampleData){}void DoWork() {UE_LOG(LogBlankProgram, Display, TEXT("ExampleAsyncTask %d Work."), ExampleData);}FORCEINLINE TStatId GetStatId() const {RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);}
};INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
{GEngineLoop.PreInit(ArgC, ArgV);UE_LOG(LogBlankProgram, Display, TEXT("UE4 Multithreading Example."));// 1. FRunnable 使用示例FRunnable * Runnable = new MyRunnable();FRunnableThread* RunnableThread = FRunnableThread::Create(Runnable, TEXT("LaLaLaDeMaXiYa!"));RunnableThread->WaitForCompletion();// 2.1 线程池异步队列FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>(1);// 交由后台控制任务开始执行时机MyTask->StartBackgroundTask();// 确保线程被执行完成MyTask->EnsureCompletion();delete MyTask;// 2.2 线程池异步队列FAsyncTask<ExampleAsyncTask>* MyTask2 = new FAsyncTask<ExampleAsyncTask>(2);// 直接在当前线程中执行MyTask2->StartSynchronousTask();// 检查任务是否完成if (MyTask2->IsDone()) {UE_LOG(LogBlankProgram, Display, TEXT("MyTask2 is Done."));}MyTask2->EnsureCompletion();delete MyTask2;// 2.3 带自动销毁的异步任务// 交由后台控制任务开始执行时机(new FAutoDeleteAsyncTask<ExampleAsyncTask>(3))->StartBackgroundTask();// 直接在当前线程中开始执行(new FAutoDeleteAsyncTask<ExampleAsyncTask>(4))->StartSynchronousTask();return 0;
}

相关文章:

UE 多线程

详细参考&#xff1a;《Exploring in UE4》多线程机制详解[原理分析] - 知乎 (zhihu.com) UE4 C基础 - 多线程 - 知乎 (zhihu.com) 多线程的好处 通过为每种事件类型的处理分配单独的线程&#xff0c;能够简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程…...

BootStrap5基础入门

BootStrap5 项目搭建 1、引入依赖 从官网 getbootstrap.com 下载 Bootstrap 5。 或者Bootstrap 5 CDN <!-- 新 Bootstrap5 核心 CSS 文件 --> <link rel"stylesheet" href"https://cdn.staticfile.org/twitter-bootstrap/5.1.1/css/bootstrap.min.c…...

企业文件防泄密软件!好用的文件加密系统推荐

由于众多企业内部都有大量的机密数据以电子文档的形式存储着&#xff0c;且传播手段多样&#xff0c;很容易造成文件泄密的问题发生。若是员工通过网络泄密重要文件&#xff0c;或是有黑客入侵窃取机密数据等&#xff0c;造成重要文件被非法查看盗取&#xff0c;都会给企业业务…...

【LLM微调范式1】Prefix-Tuning: Optimizing Continuous Prompts for Generation

论文标题&#xff1a;Prefix-Tuning: Optimizing Continuous Prompts for Generation 论文作者&#xff1a;Xiang Lisa Li, Percy Liang 论文原文&#xff1a;https://arxiv.org/abs/2101.00190 论文出处&#xff1a;ACL 2021 论文被引&#xff1a;1588&#xff08;2023/10/14&…...

实验2.1.3 交换机的远程配置

实验2.1.3 交换机的远程配置 一、任务描述二、任务分析三、实验拓扑四、具体要求五、任务实施&#xff08;一&#xff09; password认证1. 进入系统视图重命名交换机的名称为SWA2. 关闭干扰信息3. 设置vty为0-44. 设置认证方式为password5. 设置登录密码为&#xff1a;huawei6.…...

基于边缘网关构建水污染监测治理方案

绿水青山就是金山银山&#xff0c;生态环境才是人类最宝贵的财富。但是在日常生活生产中&#xff0c;总是免不了各种污水的生产、排放。针对生产生活与环境保护的均衡&#xff0c;可以借助边缘网关打造环境污水监测治理体系&#xff0c;保障生活与环境的可持续性均衡发展。 水污…...

Spring事件ApplicationEvent源码浅读

文章目录 demo应用实现基于注解事件过滤异步事件监听 源码解读总结 ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果将实现了 ApplicationListener 接口的 bean 部署到容器中&#xff0c;则每次将 ApplicationEvent 发布到…...

51单片机点阵

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、点阵是什么&#xff1f;1.点阵的原理2. 3*3 点阵显示原理3. 8*8点阵实物图4. 8*8点阵内部原理图5. 16*16点阵实物图&#xff0c;显示原理 二、使用步骤1.先…...

远程VPN登录,IPsec,VPN,win10

windows10 完美解决L2TP无法连接问题 windows10 完美解决L2TP无法连接问题 - 哔哩哔哩...

“零代码”能源管理平台:智能管理能源数据

随着能源的快速增长&#xff0c;有效管理和监控能源数据变得越来越重要。为了帮助企业更好的管理能源以及降低能源成本&#xff0c;越来越多的能源管理平台出现在市面上。 “零代码”形式的能源管理平台&#xff0c;采用IT与OT深度融合为理念&#xff0c;可进行可视化、拖拽、…...

【SA8295P 源码分析 (一)】06 - SA8295P XBL Loader 阶段 sbl1_main_ctl 函数代码分析

【SA8295P 源码分析】06 - SA8295P XBL Loader 阶段 sbl1_main_ctl 函数代码分析 一、XBL Loader 汇编源码分析1.1 解析 boot\QcomPkg\XBLLoader\XBLLoader.inf1.2 boot\QcomPkg\XBLDevPrg\ModuleEntryPoint.S:跳转 sbl1_entry 函数1.3 XBLLoaderLib\sbl1_Aarch64.s:跳转 sbl…...

Java版本spring cloud + spring boot企业电子招投标系统源代码

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及审…...

软考高级信息系统项目管理师系列论文一:论信息系统项目的整体管理

软考高级信息系统项目管理师系列论文一:论信息系统项目的整体管理 一、项目整体管理相关知识点二、摘要三、正文四、总结一、项目整体管理相关知识点 软考高级信息系统项目管理师系列之:项目整体管理...

【前端】JS - WebAPI

目 录 一.WebAPI 背景知识什么是 WebAPI什么是 APIAPI 参考文档 二.DOM 基本概念什么是 DOMDOM 树 三.获取元素querySelectorquerySelectorAll 四.事件初识基本概念事件三要素 五.操作元素获取/修改元素内容&#xff08;innerHTML&#xff09;获取/修改元素属性获取/修改样式属…...

H5+Vue3编写官网,并打包发布到同一个域名下

背景 因为html5有利于搜索引擎抓取和收录我们网站更多的内容&#xff0c;对SEO很友好&#xff0c;可以为网站带来更多的流量,并且多端适配&#xff0c;兼容性和性能都非常不错&#xff0c;所以使用h5来编写官网首页。 因为用户个人中心可以通过官网跳转&#xff0c;不需要被浏…...

黑马mysql教程笔记(mysql8教程)基础篇——函数(字符串函数、数值函数、日期函数、流程函数)

参考文章1&#xff1a;https://www.bilibili.com/video/BV1Kr4y1i7ru/ 参考文章2&#xff1a;https://dhc.pythonanywhere.com/article/public/1/ 文章目录 基础篇函数字符串函数常用函数使用示例实例&#xff1a;更新已有的所有员工号&#xff0c;使其满足5位数长度&#xff…...

Python武器库开发-基础篇(一)

前言 以Python编程为主&#xff0c;围绕渗透测试展开的一门专栏。专栏内容包括&#xff1a; Python基础编程&#xff08;Python基础、语法、对象、文件操作&#xff0c;错误和异常&#xff09;&#xff0c;Python高级编程&#xff08;正则表达式、网络编程、WEB编程&#xff0…...

Qt (QFileDialogQColorDialogQFontDialog) 对话框实战

目录 一、QFileDialog 类 (文件对话框) 二、QColorDialog 类(颜色对话框) 三、QFontDialog 类(字体对话框类) 一、QFileDialog 类 (文件对话框) QFileDialog 是 Qt 框架中的一个类&#xff0c;用于在应用程序中提供文件对话框。它允许用户选择文件或目录&#xff0c;并且可…...

2.SpringSecurity - 处理器简单说明

文章目录 SpringSecurity 返回json一、登录成功处理器1.1 统一响应类HttpResult1.2 登录成功处理器1.3 配置登录成功处理器1.4 登录 二、登录失败处理器2.1 登录失败处理器2.2 配置登录失败处理器2.3 登录 三、退出成功处理器3.1 退出成功处理器3.2 配置退出成功处理器3.3 退出…...

AGI热门方向:国内前五!AI智能体TARS-RPA-Agent落地,实在智能打造人手一个智能助理

早在 1950 年代&#xff0c;Alan Turing 就将「智能」的概念扩展到了人工实体&#xff0c;并提出了著名的图灵测试。这些人工智能实体通常被称为 —— 代理&#xff08;Agent&#xff09;。 代理这一概念起源于哲学&#xff0c;描述了一种拥有欲望、信念、意图以及采取行动能力…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

12.找到字符串中所有字母异位词

&#x1f9e0; 题目解析 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义&#xff1a; 若两个字符串包含的字符种类和出现次数完全相同&#xff0c;顺序无所谓&#xff0c;则互为…...

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

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

DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”

目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析

Java求职者面试指南&#xff1a;Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问&#xff08;基础概念问题&#xff09; 1. 请解释Spring框架的核心容器是什么&#xff1f;它在Spring中起到什么作用&#xff1f; Spring框架的核心容器是IoC容器&#…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发&#xff0c;实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构&#xff0c;服务器端使用Java Servlet处理请求&#xff0c;数据库采用MySQL存储信息&#xff0…...

Vite中定义@软链接

在webpack中可以直接通过符号表示src路径&#xff0c;但是vite中默认不可以。 如何实现&#xff1a; vite中提供了resolve.alias&#xff1a;通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...

leetcode73-矩阵置零

leetcode 73 思路 记录 0 元素的位置&#xff1a;遍历整个矩阵&#xff0c;找出所有值为 0 的元素&#xff0c;并将它们的坐标记录在数组zeroPosition中置零操作&#xff1a;遍历记录的所有 0 元素位置&#xff0c;将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

如何把工业通信协议转换成http websocket

1.现状 工业通信协议多数工作在边缘设备上&#xff0c;比如&#xff1a;PLC、IOT盒子等。上层业务系统需要根据不同的工业协议做对应开发&#xff0c;当设备上用的是modbus从站时&#xff0c;采集设备数据需要开发modbus主站&#xff1b;当设备上用的是西门子PN协议时&#xf…...