当前位置: 首页 > 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;描述了一种拥有欲望、信念、意图以及采取行动能力…...

运动品牌如何做到“全都要”?来看看安踏的答案

文 | 螳螂观察 作者 | 易不二 运动鞋服是兼具高景气和清晰格局的优质消费赛道。 中信证券给出的这一预测&#xff0c;欧睿国际也做出了更具体的测算&#xff1a;预计到2027年&#xff0c;中国运动服饰市场规模有望以约为8.7%的年复合增长率&#xff0c;突破5500亿元人民币。…...

LeetCode75——Day6

文章目录 一、题目二、题解 一、题目 151. Reverse Words in a String Given an input string s, reverse the order of the words. A word is defined as a sequence of non-space characters. The words in s will be separated by at least one space. Return a string …...

http代理有什么好处,怎么通过http代理服务安全上网呢?

通过http代理上网是一种常见的网络代理方式。http代理是指通过代理服务器进行网络连接&#xff0c;以实现隐藏自己的真实IP地址、保护个人隐私等目的。下面我们将介绍通过http代理上网的好处以及如何使用http代理服务来安全上网。 一、通过http代理上网的好处 1. 保护个人隐私 …...

vue3后台管理框架之axios二次封装

在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候 我们经常会把axios进行二次封装。 目的: 1:使用请求拦截器&#xff0c;可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数) 2:使用响应拦截器&#xf…...

你的Github账户可能被封禁!教你应对Github最新的2FA二次验证! 无地区限制, 无额外设备的全网最完美方案

1 2FA 的定义 双因素身份验证 (2FA) 是一种身份和访管理安全方法&#xff0c;需要经过两种形式的身份验证才能访河资源和数据&#xff0c;2FA使企业能够监视和帮助保护其最易受攻击的信息和网络。 2 2FA 的身份验证方法 使用双因素身份验证时有不同的身份验证方法。此处列出…...

【C语言】#define宏与函数的优劣对比

本篇文章目录 1. 预处理指令#define宏2. #define定义标识符或宏&#xff0c;要不要最后加上分号&#xff1f;3.宏的参数替换后产生的运算符优先级问题3.1 问题产生3.2 不太完美的解决办法3.3 完美的解决办法 4.#define的替换规则5. 有副作用的宏参数6. 宏与函数的优劣对比6.1 宏…...

flask基础开发知识学习

之前做了一些LLM的demo&#xff0c;接口用flask写的&#xff0c;但是涉及到后端的一些业务就感觉逻辑写的很乱&#xff0c;代码变成屎山&#xff0c;于是借助官方文档和GPT迅速补了一些知识&#xff0c;总结一下一个很小的模板 于是决定边学边重构之前的代码… 文章目录 代码结…...

内网和热点同时连接使用配置

解决如标题问题 查看当前永久路由信息 route print截图保存(重要) 截图保存(重要)查出来的永久路由&#xff0c;以防配置不成功时回退&#xff0c;回退方法就是下面的“添加永久路由” 删除当前的路由 0.0.0.0 是上面查出的网络地址 route delete 0.0.0.0内网IP信息 添加永久…...

C语言 形参、实参

定义 形参 形式上的参数&#xff0c;没有确定的值 实参 实际存在的&#xff0c;已经确定的参数&#xff0c;常量&#xff0c;变量&#xff0c;表达式&#xff0c;都是实参 区别 实参的值不随形参的变化而变化 在C语言中&#xff0c;数据传送是单向的&#xff0c;即只能把实…...

linux入门到精通-第四章-gcc编译器

目录 参考gcc概述gcc的工作流程 参考 gcc编译器 gcc概述 编辑器vi、记事本)是指我用它来写程序的 (编辑码)&#xff0c;而我们写的代码语句&#xff0c;电脑是不懂的&#xff0c;我们需要把它转成电脑能懂的语句&#xff0c;编译器就是这样的转化工具。就是说&#xff0c;我…...

有没有可以做游戏的网站/品牌全网推广

你是否正在寻找关于dataload的内容&#xff1f;让我把最权威的东西奉献给你&#xff1a;LOAD DATA [LOW_PRIORITY] [LOCAL] INFILE file_name.txt [REPLACE | IGNORE]INTO TABLE tbl_name[FIELDS[TERMINATED BY \t][OPTIONALLY] ENCLOSED BY ][ESCAPED BY \\ ]][LINES TERMINAT…...

wordpress商城微信支付宝/长沙网站seo技术厂家

在视频会议系统的应用中&#xff0c;影响视音频效果的因素主要集中在三个方面&#xff1a; 1&#xff09;网络的服务质量&#xff1b;2&#xff09;MCU和终端的性能&#xff1b;3&#xff09;会议室的设计。一、网络的服务质量&#xff08;QoS&#xff09;目前&#xff0c;视频…...

广州网络营销选择/赣州seo顾问

Linux Kernel snd_seq_write()函数本地缓冲区溢出漏洞(CVE-2018-7566)发布日期&#xff1a;2018-03-27更新日期&#xff1a;2018-04-11受影响系统&#xff1a;Linux kernel 4.15描述&#xff1a;BUGTRAQ ID: 103605CVE(CAN) ID: CVE-2018-7566Linux Kernel是Linux操作系统的内…...

深圳设计深圳设计公司/seo01

功能:在断点触发后立即执行指令. 使用,可以用于在触发后输出对应的并继续. commands [list...] ... command-list ... endlist表示断点集合.无list表示给最新设置的断点添加. 有就是指定. command-list就是gdb指令. end表示末尾. 添加上面的格式. 删除就是无command-list. …...

网站建设公司东莞/昭通网站seo

业务需要 在类里附加一些属性。为了实现松耦合 。故在每个类里设置Attribute。 做了1天半 简单搞了个demo 代码 1 ///<summary>2 ///记录日志扩展属性3 ///</summary>4 [AttributeUsage(AttributeTargets.All,AllowMultiplefalse,Inheritedfalse)]5 publicclassLog…...

做B2C独立网站的话需要做海外仓吗/最近几天发生的新闻大事

协调节点策略 kafka通过zookeeper来进行协调&#xff0c;而rocketMq通过自身的namesrv进行协调。rocketMq在协调节点的设计上显得更加轻量。 kafka在具备选举功能&#xff0c;在Kafka里面&#xff0c;Master/Slave的选举&#xff0c;有2步。第1步&#xff0c;先通过ZK在所有机…...