UE5.4异步通信实战:从零构建轻量级消息队列框架

Zephyr zbus:微内核时代的轻量级通信革命 Zephyr zbus:嵌入式系统的轻量级通信框架 摘要:Zephyr zbus是一种基于发布-订阅模式的轻量级线程间通信机制,专为资源受限的嵌入式系统设计。相比传统IPC方式,zbus通过通道(Channel)概念实现松耦合通信,支持观察者(Observer)模式,包括订阅者(Subscriber)和监听器(Listener)两种接收方式。该框架解决了嵌入式开发中常见的紧耦合、代码重复和维护困难等问题,特别适用于物联网设备的多模块通信场景。zbus具有内存占用小、类型安全、支持动态订阅等特点,为智能家居、 阅读详情

1. 项目概述:为什么要在UE5.4里造一个消息队列?

如果你在UE5里做过稍微复杂点的逻辑,尤其是涉及到不同系统(比如UI、Gameplay、网络、AI)之间通信,肯定遇到过这样的头疼事:A系统想告诉B系统“玩家血量变了”,结果B系统可能还没初始化好,或者正在处理别的逻辑,直接调用接口就崩了。又或者,你想在下一帧再执行某个操作,或者让一个操作延迟几秒执行,难道就靠一个 Delay 节点或者 Timer 硬等?代码耦合度高,调试起来像一团乱麻。

这就是消息队列(Message Queue, MQ)要解决的问题。它本质上是一个“中间人”,发送方(Producer)把消息丢到队列里就可以不管了,接收方(Consumer)按照自己的节奏从队列里取消息处理。两者完全解耦,发送方不知道、也不关心是谁来处理,处理方也不知道消息具体从哪来。在大型游戏架构里,这种异步、解耦的通信方式几乎是必需品。

你可能会问,UE不是有 Delegate (委托)和 Event (事件)吗?没错,它们是UE里常用的通信机制,但它们是 同步 的。当 Broadcast 一个多播委托时,所有绑定的函数会立刻、依次被执行。如果某个监听者处理时间过长,或者崩溃了,会直接阻塞整个调用链。而消息队列是 异步 的,消息被放入队列后,发送方就返回了,处理是延后的、可控的。

所以,在UE5.4里实现一个消息队列,不是为了替代现有机制,而是 补全 UE在异步、可靠、跨线程消息传递方面的能力。它能帮你:

  1. 彻底解耦系统 :Gameplay模块发个“任务完成”消息,UI模块和成就系统各自监听处理,互不干扰。
  2. 实现延迟与定时逻辑 :将消息投递到“延迟队列”或“定时队列”,实现精准的延迟回调,比单纯用Timer更清晰、更易管理。
  3. 缓冲与削峰填谷 :瞬间产生大量事件(如击中特效、音效请求),可以先存入队列,由消费端逐帧消化,避免同一帧卡顿。
  4. 简化跨线程通信 :将需要从工作线程(如资源加载、网络接收)传递到游戏线程的数据包装成消息,安全地投递到主线程队列处理。

接下来,我们就从零开始,在UE5.4里打造一个轻量、高效、易用的消息队列系统。

2. 核心设计:打造一个适合UE的轻量级MQ框架

直接照搬RocketMQ、Kafka那种企业级消息中间件到游戏里是杀鸡用牛刀,太重了。我们需要的是一个贴合UE引擎习惯、开销小、功能聚焦的轻量级实现。核心设计围绕以下几个点展开:

2.1 消息体设计:如何承载任意数据?

消息队列里的“消息”必须是一个通用的载体。在C++里,我们很自然地想到使用 USTRUCT ,因为它可以被UE的反射系统部分支持,也方便蓝图使用。但为了极致的灵活性和性能,我们选择使用 TSharedPtr 来包装一个消息基类。

首先,定义一个所有消息的基类:

// MQMessage.h
#pragma once

#include "CoreMinimal.h"
#include "MQMessage.generated.h"

USTRUCT(BlueprintType)
struct MYMQ_API FMQMessageBodyBase
{
    GENERATED_BODY()

public:
    virtual ~FMQMessageBodyBase() = default;
    // 可以定义一些公共接口,比如获取消息类型名,用于调试或路由
    virtual FString GetMessageType() const { return TEXT("Base"); }
};

// 使用一个简单的结构体包装消息体和元数据
USTRUCT(BlueprintType)
struct MYMQ_API FMQMessage
{
    GENERATED_BODY()

public:
    // 消息主题(或频道),用于区分消息种类,如 “Player.Health”, “System.Achievement”
    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    FName Topic;

    // 发送时间戳(引擎时间)
    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    float SendTime;

    // 延迟处理的时间(秒),0表示立即处理
    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    float Delay = 0.0f;

    // 实际的消息数据,使用共享指针以便安全传递和存储
    TSharedPtr<FMQMessageBodyBase> Body;
};

这里的关键是 Body 成员,它是一个指向基类的智能指针。我们可以为不同的数据类型创建派生类:

// 示例:一个携带整型数据的消息体
USTRUCT(BlueprintType)
struct FMQMessageBody_Int : public FMQMessageBodyBase
{
    GENERATED_BODY()
    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    int32 Value = 0;

    virtual FString GetMessageType() const override { return TEXT("Int"); }
};

// 示例:一个携带字符串和向量的消息体
USTRUCT(BlueprintType)
struct FMQMessageBody_TransformEvent : public FMQMessageBodyBase
{
    GENERATED_BODY()
    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    FString EventName;

    UPROPERTY(BlueprintReadWrite, Category = "MQ")
    FVector Location;

    virtual FString GetMessageType() const override { return TEXT("TransformEvent"); }
};

这样设计的好处是 类型安全 和 扩展性 极强。任何USTRUCT都可以作为消息体,通过模板和继承,我们可以在编译期或运行时安全地转换类型。

注意 : Body 没有使用 UPROPERTY ,因为 TSharedPtr<FMQMessageBodyBase> 不是UE反射直接支持的UObject类型。这没关系,因为我们的消息主要在C++侧创建和解析,蓝图可以通过定义好的、带有 UPROPERTY 的派生消息体结构来交互。或者,我们可以提供一个辅助函数将常见数据类型打包成消息。

2.2 队列核心:线程安全与存储选择

消息队列的核心数据结构就是一个“队列”。UE提供了 TQueue 这个线程安全的模板容器,它天生就是为了生产者-消费者场景设计的,支持 Enqueue (入队)和 Dequeue (出队)操作,并且这些操作是线程安全的。

我们的主消息队列类将包含一个 TQueue :

// MQCore.h
#pragma once

#include "CoreMinimal.h"
#include "MQMessage.h"
#include "Async/AsyncWork.h"
#include "Containers/Queue.h"

class MYMQ_API FMQCore : public FRunnable
{
public:
    static FMQCore& Get();

    // 初始化与销毁
    void Initialize();
    void Shutdown();

    // 发布消息
    void Publish(const FName& InTopic, const TSharedPtr<FMQMessageBodyBase>& InBody, float InDelay = 0.0f);

    // 订阅消息
    FDelegateHandle Subscribe(const FName& InTopic, const TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>& InCallback);
    void Unsubscribe(const FName& InTopic, const FDelegateHandle& InHandle);

private:
    FMQCore();
    ~FMQCore();

    // FRunnable 接口,用于驱动消息处理的专用线程(可选)
    virtual uint32 Run() override;
    virtual void Stop() override;
    virtual void Exit() override;

    // 内部处理函数
    void ProcessPendingMessages();

    // 主消息队列(线程安全)
    TQueue<TSharedPtr<FMQMessage>, EQueueMode::Mpsc> MainMessageQueue;

    // 延迟消息的优先队列(按处理时间排序)
    struct FDelayedMessage
    {
        TSharedPtr<FMQMessage> Message;
        float ExecuteTime;
        bool operator<(const FDelayedMessage& Other) const { return ExecuteTime > Other.ExecuteTime; } // 小顶堆
    };
    TArray<FDelayedMessage> DelayedMessages;

    // 订阅者列表 Key: Topic, Value: 回调函数数组
    TMap<FName, TArray<TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>>> Subscribers;

    // 线程控制
    FRunnableThread* WorkerThread = nullptr;
    FThreadSafeBool bIsRunning = false;

    // 用于线程同步的事件
    FEvent* Semaphore = nullptr;

    FCriticalSection SubscribersCriticalSection;
    FCriticalSection DelayedMessagesCriticalSection;
};

这里有几个关键设计决策:

  1. 使用 TQueue :它内部自带了锁, Mpsc (Multi-producer, Single-consumer)模式非常适合我们这里可能有多个系统发送消息,但由一个统一消费者(我们的消息处理循环)来处理的场景。
  2. 延迟消息处理 :我使用了一个 TArray 配合堆算法( operator< 定义)来实现优先队列。 ProcessPendingMessages 函数每一帧或每次循环时,会检查 DelayedMessages 堆顶的消息是否到了该执行的时间。
  3. 订阅模式 :使用 TMap 来存储主题和回调函数的映射。这里回调使用了 TFunction ,它比UE的委托更轻量,也更适合C++ lambda。注意对 Subscribers 的读写需要用 FCriticalSection (临界区)保护,因为订阅/取消订阅可能发生在任何线程。
  4. 专用线程 vs 主线程Tick :我提供了两种驱动方式。一种是继承 FRunnable ,创建一个专用工作线程来不断处理消息( Run 函数),适合需要后台持续处理的逻辑。另一种更简单、更安全(尤其对于需要操作游戏对象的逻辑)的是在游戏主线程的 Tick 中调用 ProcessPendingMessages 。示例中展示了线程方案,但我会强烈建议新手先从主线程Tick方案开始。

实操心得 :对于游戏开发, 我强烈建议将消息的最终消费放在游戏线程(GameThread) 。因为大部分UE的API(如生成Actor、修改UI、播放音效)都必须在游戏线程调用。我们的消息队列可以负责接收来自任何线程的消息( Publish 是线程安全的),但在 ProcessPendingMessages 中,当真正执行订阅者的回调时,应该确保回调函数是在游戏线程中执行的。这可以通过将回调函数包装成 AsyncTask 投递到游戏线程来实现,或者直接约定订阅者回调只来自主线程Tick驱动。

2.3 订阅与分发:如何高效通知订阅者?

当一条消息到达处理时间,我们需要找到所有订阅了该消息主题(Topic)的监听者,并调用他们的回调函数。这个过程必须高效,因为可能每帧都有大量消息。

我们在 FMQCore 中实现 ProcessPendingMessages 的核心逻辑:

void FMQCore::ProcessPendingMessages()
{
    // 1. 处理延迟消息
    float CurrentTime = GetWorld() ? GetWorld()->GetTimeSeconds() : FPlatformTime::Seconds();
    {
        FScopeLock Lock(&DelayedMessagesCriticalSection);
        while (DelayedMessages.Num() > 0 && DelayedMessages[0].ExecuteTime <= CurrentTime)
        {
            TSharedPtr<FMQMessage> ReadyMessage = DelayedMessages[0].Message;
            DelayedMessages.RemoveAt(0, 1, false); // 保持堆结构
            if (ReadyMessage.IsValid())
            {
                MainMessageQueue.Enqueue(ReadyMessage);
            }
            // 重新调整堆
            if (DelayedMessages.Num() > 0)
            {
                DelayedMessages.Heapify();
            }
        }
    }

    // 2. 处理主队列中的消息
    TSharedPtr<FMQMessage> NextMessage;
    while (MainMessageQueue.Dequeue(NextMessage))
    {
        if (!NextMessage.IsValid()) continue;

        FName Topic = NextMessage->Topic;
        TSharedPtr<FMQMessageBodyBase> Body = NextMessage->Body;

        // 查找订阅者
        TArray<TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>> CallbacksToExecute;
        {
            FScopeLock Lock(&SubscribersCriticalSection);
            if (TArray<TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>>* FoundCallbacks = Subscribers.Find(Topic))
            {
                CallbacksToExecute = *FoundCallbacks; // 复制一份,避免在回调中修改原数组导致问题
            }
        }

        // 执行回调
        for (auto& Callback : CallbacksToExecute)
        {
            if (Callback)
            {
                // 重要:这里假设回调是线程安全的,或者我们确保它在GameThread执行。
                // 更安全的做法是:AsyncTask(ENamedThreads::GameThread, [Callback, Body](){ Callback(Body); });
                Callback(Body);
            }
        }
    }
}

关键点与避坑指南 :

  • 回调执行环境 :如代码注释所述,直接执行 Callback(Body) 存在风险。如果 Publish 来自工作线程,而回调函数里调用了UE游戏线程的API,程序会崩溃。 安全的做法 是在 Publish 时或 ProcessPendingMessages 中,判断当前线程,如果不是游戏线程,则使用 AsyncTask 或 FFunctionGraphTask 将回调的执行派发到游戏线程。这是实现中 最容易出错的地方 。
  • 订阅者列表拷贝 :在执行回调前,我们复制了一份回调列表( CallbacksToExecute )。这是因为回调函数本身可能会执行 Unsubscribe 操作,直接遍历和修改 Subscribers 映射会导致迭代器失效,程序崩溃。这是一个经典的并发编程问题。
  • 性能考量 :对于高频消息(如每帧的 Tick ),频繁地查找 TMap 和拷贝数组可能成为瓶颈。如果性能敏感,可以考虑使用 TArray 存储订阅者,或者对Topic使用哈希值进行比较。但对于绝大多数游戏逻辑,这个开销是可以接受的。

3. 分步实现:从零搭建消息队列模块

理论说完了,我们动手创建一个UE5.4插件或模块。这里我以创建名为 MyMessageQueue 的游戏模块为例。

3.1 创建模块与基础类

  1. 在你的项目源码目录( Source/YourProjectName/ )下创建新文件夹 MyMessageQueue 。
  2. 创建 MyMessageQueue.Build.cs 文件,定义模块依赖:
    // MyMessageQueue.Build.cs
    using UnrealBuildTool;
    
    public class MyMessageQueue : ModuleRules
    {
        public MyMessageQueue(ReadOnlyTargetRules Target) : base(Target)
        {
            PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
    
            PublicDependencyModuleNames.AddRange(new string[] { "Core" });
    
            PrivateDependencyModuleNames.AddRange(new string[] {
                "CoreUObject",
                "Engine",
                "Slate",
                "SlateCore",
            });
        }
    }
    
  3. 创建 MyMessageQueue.h 和 MyMessageQueue.cpp ,实现模块启动和关闭,并在其中初始化我们的 FMQCore 单例。
    // MyMessageQueue.h
    #pragma once
    #include "Modules/ModuleManager.h"
    
    class FMyMessageQueueModule : public IModuleInterface
    {
    public:
        virtual void StartupModule() override;
        virtual void ShutdownModule() override;
    };
    
    // MyMessageQueue.cpp
    #include "MyMessageQueue.h"
    #include "MQCore.h"
    
    void FMyMessageQueueModule::StartupModule()
    {
        FMQCore::Get().Initialize();
    }
    
    void FMyMessageQueueModule::ShutdownModule()
    {
        FMQCore::Get().Shutdown();
    }
    IMPLEMENT_MODULE(FMyMessageQueueModule, MyMessageQueue)
    
  4. 将前面设计的 MQMessage.h 、 MQCore.h 及其对应的 .cpp 文件放入模块目录。
  5. 在 MQCore.cpp 中实现单例模式和初始化逻辑:
    // MQCore.cpp 部分实现
    FMQCore& FMQCore::Get()
    {
        static FMQCore Instance;
        return Instance;
    }
    
    void FMQCore::Initialize()
    {
        if (bIsRunning) return;
        bIsRunning = true;
    
        // 方案一:创建工作者线程(处理消息)
        // WorkerThread = FRunnableThread::Create(this, TEXT("MQWorkerThread"), 0, TPri_BelowNormal);
    
        // 方案二(推荐):注册到游戏线程的Tick
        // 需要在某个游戏线程对象(如GameInstance或PlayerController)的BeginPlay中调用BindToTick
    }
    
    void FMQCore::Shutdown()
    {
        bIsRunning = false;
        if (Semaphore)
        {
            Semaphore->Trigger();
        }
        if (WorkerThread)
        {
            WorkerThread->WaitForCompletion();
            delete WorkerThread;
            WorkerThread = nullptr;
        }
        // 清空队列和订阅者
        ...
    }
    

3.2 实现线程安全的消息发布与订阅

现在实现最关键的 Publish 和 Subscribe 函数。

// MQCore.cpp - Publish 实现
void FMQCore::Publish(const FName& InTopic, const TSharedPtr<FMQMessageBodyBase>& InBody, float InDelay)
{
    if (!bIsRunning || !InBody.IsValid()) return;

    TSharedPtr<FMQMessage> NewMessage = MakeShared<FMQMessage>();
    NewMessage->Topic = InTopic;
    NewMessage->SendTime = FPlatformTime::Seconds();
    NewMessage->Body = InBody;

    if (InDelay > 0.0f)
    {
        FDelayedMessage DelayedMsg;
        DelayedMsg.Message = NewMessage;
        DelayedMsg.ExecuteTime = NewMessage->SendTime + InDelay;

        FScopeLock Lock(&DelayedMessagesCriticalSection);
        DelayedMessages.HeapPush(DelayedMsg);
    }
    else
    {
        // 立即消息,放入主队列
        MainMessageQueue.Enqueue(NewMessage);
        // 如果有工作者线程,通知它
        if (Semaphore)
        {
            Semaphore->Trigger();
        }
    }
}

// MQCore.cpp - Subscribe 实现
FDelegateHandle FMQCore::Subscribe(const FName& InTopic, const TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>& InCallback)
{
    FScopeLock Lock(&SubscribersCriticalSection);
    TArray<TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>>& CallbackArray = Subscribers.FindOrAdd(InTopic);
    CallbackArray.Add(InCallback);

    // 返回一个可用于取消订阅的句柄。这里简单使用回调在数组中的索引作为标识。
    // 更健壮的做法是生成一个唯一的句柄。
    return FDelegateHandle(CallbackArray.Num() - 1);
}

void FMQCore::Unsubscribe(const FName& InTopic, const FDelegateHandle& InHandle)
{
    FScopeLock Lock(&SubscribersCriticalSection);
    if (TArray<TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)>>* FoundCallbacks = Subscribers.Find(InTopic))
    {
        int32 Index = InHandle.GetIndex(); // 假设我们实现了GetIndex
        if (FoundCallbacks->IsValidIndex(Index))
        {
            // 注意:这里只是置空,而不是移除,避免破坏其他句柄的索引。
            // 可以考虑在ProcessPendingMessages中跳过空回调,或定期清理。
            (*FoundCallbacks)[Index] = nullptr;
        }
    }
}

3.3 绑定到游戏主循环(Tick)

对于游戏开发,让消息队列在主线程 Tick 是最稳妥的。我们可以创建一个 UObject 来负责每帧驱动。

// MQActorComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MQActorComponent.generated.h"

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYMQ_API UMQActorComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UMQActorComponent();
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};
// MQActorComponent.cpp
#include "MQActorComponent.h"
#include "MQCore.h"

UMQActorComponent::UMQActorComponent()
{
    PrimaryComponentTick.bCanEverTick = true;
    PrimaryComponentTick.TickGroup = TG_PrePhysics; // 在物理模拟前处理消息
}

void UMQActorComponent::BeginPlay()
{
    Super::BeginPlay();
    FMQCore::Get().Initialize();
}

void UMQActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    // 每帧驱动消息处理
    FMQCore::Get().ProcessPendingMessages();
}

void UMQActorComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    FMQCore::Get().Shutdown();
    Super::EndPlay(EndPlayReason);
}

将这个组件添加到你的 GameMode 、 PlayerController 或某个长期存在的 Actor 上,消息队列就会自动每帧处理了。

4. 实战应用:在游戏逻辑中收发消息

框架搭好了,怎么用呢?我们来看几个游戏内的典型场景。

4.1 场景一:UI与Gameplay解耦

假设玩家血量发生变化,需要更新UI血条并可能触发一个屏幕特效。

传统紧耦合方式 : PlayerHealthComponent 直接调用 UIPlayerHUD->UpdateHealthBar(CurrentHealth) 和 CameraShakeManager->PlayLowHealthEffect() 。 PlayerHealthComponent 需要知道UI和特效管理器的具体引用。

使用消息队列解耦 :

  1. 定义消息 :
    USTRUCT(BlueprintType)
    struct FMQMessageBody_HealthChanged : public FMQMessageBodyBase
    {
        GENERATED_BODY()
        UPROPERTY(BlueprintReadWrite, Category = "MQ")
        float CurrentHealth;
        UPROPERTY(BlueprintReadWrite, Category = "MQ")
        float MaxHealth;
        UPROPERTY(BlueprintReadWrite, Category = "MQ")
        AActor* DamageInstigator; // 可选
    };
    
  2. 发送消息(在PlayerHealthComponent中) :
    void UPlayerHealthComponent::TakeDamage(float DamageAmount)
    {
        CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth);
        
        auto HealthMsg = MakeShared<FMQMessageBody_HealthChanged>();
        HealthMsg->CurrentHealth = CurrentHealth;
        HealthMsg->MaxHealth = MaxHealth;
        
        FMQCore::Get().Publish(FName(TEXT("Player.Health.Changed")), HealthMsg);
        
        if (CurrentHealth <= 0.0f)
        {
            auto DeathMsg = MakeShared<FMQMessageBody_Base>(); // 甚至可以用空消息体
            FMQCore::Get().Publish(FName(TEXT("Player.Death")), DeathMsg);
        }
    }
    
  3. 接收消息(在UI和特效系统中) :
    // 在UIPlayerHUD的初始化中订阅
    void AUIPlayerHUD::BeginPlay()
    {
        Super::BeginPlay();
        HealthChangedHandle = FMQCore::Get().Subscribe(
            FName(TEXT("Player.Health.Changed")),
            [this](const TSharedPtr<FMQMessageBodyBase>& Body) {
                if (auto HealthBody = StaticCastSharedPtr<FMQMessageBody_HealthChanged>(Body))
                {
                    // 注意:这个Lambda可能在非游戏线程被调用!
                    // 安全的做法:用AsyncTask切换到GameThread再更新UI
                    AsyncTask(ENamedThreads::GameThread, [this, HealthBody](){
                        UpdateHealthBar(HealthBody->CurrentHealth, HealthBody->MaxHealth);
                    });
                }
            }
        );
    }
    
    void AUIPlayerHUD::EndPlay(const EEndPlayReason::Type EndPlayReason)
    {
        FMQCore::Get().Unsubscribe(FName(TEXT("Player.Health.Changed")), HealthChangedHandle);
        Super::EndPlay(EndPlayReason);
    }
    
    // 在CameraShakeManager中订阅
    void ACameraShakeManager::BeginPlay()
    {
        Super::BeginPlay();
        FMQCore::Get().Subscribe(
            FName(TEXT("Player.Health.Changed")),
            [this](const TSharedPtr<FMQMessageBodyBase>& Body) {
                if (auto HealthBody = StaticCastSharedPtr<FMQMessageBody_HealthChanged>(Body))
                {
                    if (HealthBody->CurrentHealth / HealthBody->MaxHealth < 0.3f)
                    {
                        AsyncTask(ENamedThreads::GameThread, [this](){
                            PlayLowHealthEffect();
                        });
                    }
                }
            }
        );
    }
    

这样做的好处 :

  • PlayerHealthComponent 不再依赖任何UI或特效类,只负责发消息。
  • UI系统和特效系统独立监听自己关心的消息,可以随时热插拔。
  • 未来要增加一个新系统(比如语音提示“血量低”),只需要订阅同一个消息,无需修改 PlayerHealthComponent 。

4.2 场景二:实现延迟与定时任务

你想让一个爆炸效果在3秒后触发,或者让一个NPC在对话结束后5秒走到某个位置。

// 3秒后触发爆炸
auto ExplosionMsg = MakeShared<FMQMessageBody_TransformEvent>();
ExplosionMsg->EventName = TEXT("BigExplosion");
ExplosionMsg->Location = TargetLocation;
FMQCore::Get().Publish(FName(TEXT("VFX.Trigger")), ExplosionMsg, 3.0f); // 关键:第三个参数是延迟

// NPC对话结束,5秒后移动
auto MoveMsg = MakeShared<FMQMessageBody_String>();
MoveMsg->Value = TEXT("MoveToPoint_B");
FMQCore::Get().Publish(FName(TEXT("AI.NPC.Move")), MoveMsg, 5.0f);

在VFX系统或AI系统中,正常订阅 VFX.Trigger 和 AI.NPC.Move 主题即可。消息队列会自动在3秒和5秒后将消息投递到处理队列,完全不需要你手动管理 Timer 句柄或担心 Timer 回调的上下文安全问题。

4.3 场景三:跨线程数据传递

你在工作线程中加载了一个纹理,需要在主线程中应用它。

// 在工作线程(AsyncLoad线程)中
void UMyAsyncLoader::LoadTextureCompleted(UTexture2D* LoadedTexture)
{
    if (IsInGameThread())
    {
        ApplyTexture(LoadedTexture);
    }
    else
    {
        // 创建消息体,携带纹理指针。注意:需要确保LoadedTexture的生命周期。
        auto TextureMsg = MakeShared<FMQMessageBody_TextureLoaded>();
        TextureMsg->Texture = LoadedTexture; // 假设FMQMessageBody_TextureLoaded有UTexture2D*成员
        
        // 发布消息,延迟为0,表示尽快处理
        FMQCore::Get().Publish(FName(TEXT("Render.Texture.Ready")), TextureMsg);
        
        // 注意:这里LoadedTexture的引用计数需要管理好,或者消息体持有TSoftObjectPtr。
    }
}

// 在主线程的某个Renderer组件中
void UMyRendererComponent::BeginPlay()
{
    Super::BeginPlay();
    FMQCore::Get().Subscribe(
        FName(TEXT("Render.Texture.Ready")),
        [this](const TSharedPtr<FMQMessageBodyBase>& Body) {
            // 因为ProcessPendingMessages是在主线程Tick中调用的,所以这里一定在主线程!
            if (auto TextureBody = StaticCastSharedPtr<FMQMessageBody_TextureLoaded>(Body))
            {
                ApplyTexture(TextureBody->Texture); // 安全地应用纹理
            }
        }
    );
}

消息队列充当了一个 线程安全的邮箱 ,工作线程把“信”(消息)投进去,主线程定期取“信”处理,完美解决了跨线程访问UE对象的问题。

5. 高级话题与性能优化

一个基础的消息队列已经能解决80%的问题。当你的游戏规模变大,消息量激增时,可以考虑以下优化和扩展。

5.1 消息优先级

有些消息需要被优先处理(如输入消息、网络同步消息),有些则可以延后(如日志消息、非关键特效)。我们可以为 FMQMessage 增加一个 Priority 字段,并使用优先级队列(如 TBinaryHeap )来代替简单的 TQueue 。

UENUM(BlueprintType)
enum class EMQPriority : uint8
{
    RealTime, // 实时,如输入
    High,     // 高,如游戏状态更新
    Normal,   // 普通,大多数游戏事件
    Low,      // 低,如日志、分析事件
};

// 在FMQMessage中添加
UPROPERTY(BlueprintReadWrite, Category = "MQ")
EMQPriority Priority = EMQPriority::Normal;

// 在FMQCore中使用TArray<TSharedPtr<FMQMessage>>并自定义排序规则的堆,或者维护多个不同优先级的TQueue。

5.2 消息过滤与路由

有时订阅者只关心特定条件下的消息。例如,一个区域触发器只关心发生在它边界内的事件。可以在订阅时传递一个过滤函数(Predicate)。

// 扩展Subscribe函数,增加一个过滤条件
FDelegateHandle SubscribeWithFilter(const FName& InTopic, 
                                     TFunction<bool(const TSharedPtr<FMQMessageBodyBase>&)> InFilter,
                                     TFunction<void(const TSharedPtr<FMQMessageBodyBase>&)> InCallback);

// 在ProcessPendingMessages中,执行回调前先检查过滤条件
if (InFilter && !InFilter(Body)) continue;
Callback(Body);

5.3 对象生命周期与内存管理

这是使用消息队列(以及任何基于回调的系统)必须警惕的 内存泄漏 问题,与Handler内存泄漏原理类似。

问题根源 :

  1. 你订阅消息时,回调函数(常常是Lambda)捕获了 this 指针(即你的UObject)。
  2. 你的UObject被销毁了( EndPlay 或 ConditionalBeginDestroy ),但 没有取消订阅 。
  3. 消息队列的订阅者列表里,还保存着一个指向已销毁对象的回调函数(“悬空指针”)。
  4. 当未来有消息发布时,系统会尝试调用这个回调,导致访问违例(崩溃)或执行无意义的逻辑。

解决方案 :

  • 严格配对 :在 BeginPlay 中订阅,在 EndPlay 中 必须 取消订阅。这是铁律。
  • 使用弱引用 :在Lambda中捕获对象的弱引用。
    TWeakObjectPtr<AUIPlayerHUD> WeakThis(this);
    SubscribeHandle = FMQCore::Get().Subscribe(Topic, [WeakThis](auto& Body){
        if (AUIPlayerHUD* StrongThis = WeakThis.Get())
        {
            StrongThis->HandleMessage(Body);
        }
        // 如果对象已销毁,WeakThis.Get()返回nullptr,什么也不做。
    });
    
  • 自动清理 :在 FMQCore 的 ProcessPendingMessages 中,执行回调前检查弱引用是否有效,无效则从订阅者列表中移除。或者定期清理订阅者列表中的空回调。
  • 使用UE的委托系统 :可以考虑用 FDelegate 和 FDelegateHandle 来管理订阅,它内部有更完善的清理机制,但会比 TFunction 重一些。

5.4 性能分析与监控

当消息量很大时,需要监控队列深度、处理延迟等。

  • 添加统计 :在 FMQCore 中添加计数器,统计每秒发布/处理的消息数、队列平均长度、最大延迟等。
  • 条件编译 :在开发版本中,可以添加详细的日志输出,记录每条消息的流向和处理时间。
  • 蓝图调试支持 :可以暴露一些函数到蓝图,让设计师也能查看当前活跃的主题和订阅者数量,便于调试复杂的事件流。

6. 常见问题排查与调试技巧

在实际使用中,你肯定会遇到各种问题。这里记录一些典型坑位和解决方法。

问题1:消息发了,但没收到。

  • 检查主题名 :确保发布和订阅的主题 FName 完全一致,包括大小写。 FName 是大小写不敏感的,但最好保持一致。使用 FName(TEXT(“My.Topic”)) 创建。
  • 检查订阅时机 :是否在接收者初始化(如 BeginPlay )之前就发布了消息?确保订阅发生在可能的发布之前。对于游戏开始时就存在的对象,在 BeginPlay 中订阅是安全的。
  • 检查生命周期 :接收者对象是否已经被销毁( EndPlay )?如果销毁了,其订阅的回调应该被移除。
  • 检查线程 :消息是在哪个线程发布的?处理回调的线程是哪个?如果回调里调用了游戏线程API,但回调在工作线程执行,就会崩溃或静默失败。 确保回调执行在正确的线程 。

问题2:程序崩溃,报错访问违例。

  • 悬空指针 :这是最常见的原因。回顾 5.3 对象生命周期 部分。使用 TWeakObjectPtr 并在回调中检查有效性。
  • 在回调中取消订阅 :如果你在回调函数内部执行了 Unsubscribe ,并且遍历订阅者列表的代码没有做保护(如我们之前做的拷贝列表),会导致迭代器失效崩溃。确保我们的 ProcessPendingMessages 中先拷贝回调列表再执行。
  • 消息体类型转换错误 :使用 StaticCastSharedPtr 向下转型时,确保消息体类型匹配。可以在消息基类中加入 MessageType 字符串,在转换前先进行判断。

问题3:性能下降,感觉卡顿。

  • 单帧消息爆炸 :如果某一帧发布了成百上千条消息(比如粒子碰撞事件),而处理这些消息的回调函数本身很耗时(如播放音效、生成Actor),就会导致卡顿。 解决方案 :对于高频事件,考虑在发布端进行合并或节流。例如,收集一秒内所有的“得分”消息,合并成一条“总得分更新”消息再发布。
  • 订阅者过多 :如果一个热门主题(如 ”Tick” )有上百个订阅者,每帧遍历执行所有回调开销很大。 解决方案 :审视设计,是否真的需要这么多订阅者?能否合并逻辑?对于高频主题,回调函数应尽量轻量。
  • 频繁的内存分配 :每发布一条消息都 MakeShared 一个消息体,会产生大量的小内存分配。 解决方案 :对于极其高频的消息,可以考虑使用对象池( TObjectPool )来复用消息体对象。

调试技巧 :

  • 可视化日志 :在 Publish 和回调执行处添加 UE_LOG ,并附带消息主题和简要内容。使用 UE_LOG(LogTemp, Verbose, TEXT(“Publishing topic %s”), *InTopic.ToString()); 。
  • 蓝图调试节点 :创建一些简单的蓝图函数库节点,如 Print Active MQ Topics 、 Get MQ Queue Size ,暴露给设计师,在运行时查看消息队列状态。
  • 断点与探查 :在 FMQCore::ProcessPendingMessages 函数开始处打条件断点,当特定主题的消息出现时中断,可以一步步跟踪消息的分发流程。

实现一个消息队列,就像是给你的游戏世界铺设了一套神经系统。各个器官(系统)不再需要直接纠缠在一起,而是通过神经信号(消息)来协同工作。一开始可能会觉得多了一层抽象有点麻烦,但当你需要修改、调试、扩展系统时,你会感谢当初引入了这套解耦的机制。尤其是在UE5.4这样功能庞杂的引擎里,良好的架构是项目能健康迭代下去的基石。从今天这个小轮子开始,尝试用消息的思维去设计你的下一个功能模块吧。

消息队列系列 五篇 作者:禅与计算机程序设计艺术 1.简介 消息队列(Message Queue)是一种应用在分布式系统中的异步通信模型。简单来说,它是将发送者和接收者之间的通信延迟到稍后再进行的一种通信方式。发送者通过发送消息到队列中,然后由接收者从队列中获取并处理这些消息。消息队列可以帮助解决系统间的同步问题,使得不同 阅读详情

相关推荐

Androiud安卓链接sqllite的代码使用案例

【代码】Androiud安卓链接sqllite的代码使用案例。

laterstage的博客 1326

Go 消息队列学习指南

本文介绍了消息队列的基础概念及在Go语言中的实现方案。消息队列通过生产者-消费者模式实现异步通信,核心组件包括队列、代理服务器和路由机制。Go语言支持多种消息队列技术,文中对比了RabbitMQ、Kafka、NATS、Redis Streams和NSQ的特点与适用场景,并提供了RabbitMQ和Kafka的具体代码示例,包括生产者发送消息和消费者接收消息的实现。这些技术能有效实现系统解耦、流量削峰和异步处理,为Go开发者提供了可靠的消息通信解决方案。

GitHub个人仓库地址:https://github.com/feiyuluoye ; 个人塔罗牌占卜平台:http://118.126.82.12:8080/ ; 欢迎广大Coding友共同交流讨论。 740

这可是全网网工数通方向学习路线最完整,最详细的版本,没有之一

文章篇幅较长,耐心看完你一定有所收获。

最铁头的网工博客 2万+

227-消息队列MQ

消息队列(Message Queue, MQ)是一种跨进程通信(IPC)或分布式系统的异步消息传递机制。它通过先进先出(FIFO)的方式存储和传输消息,使系统中的不同组件可以解耦并高效地进行通信。

Lich Howger 883

无需上下文,MCP新机制让大模型主动“提需”:节约 98% token 开销,并保持准确率基本不变!

值得注意的是,MCP 协议定义的服务器描述通常较为简洁。为了提升匹配效果,我们引入 Qwen2.5-72b-Instruct 模型,对所有服务器的原始资料(通常为 ReadMe 文档)进行总结提炼,参考其 capabilities、features、usage patterns 等要素,生成服务器的 summary 信息,作为相对详尽的描述文本用于匹配。然而,现有的 MCP 调用机制下,所有工具的描述信息都被注入大模型的系统提示词,导致了非常长的上下文开销,也为大模型的表现带来了影响,降低了模型的性能。

我爱计算机视觉 1165

《C/C++等级考试(1~8级)历届真题解析》专栏总目录

中国电子学会《全国青少年软件编程等级考试》C/C++编程(1~8级)历届真题解析。订阅后可阅读专栏内所有真题解析,真题持续更新中!

人生就是一个不断学习的过程 2万+

Apache ShenYu终极指南:如何集成ActiveMQ实现高性能消息队列代理

Apache ShenYu是一款基于Java的原生API网关,专注于服务代理、协议转换和API治理。本指南将详细介绍如何在ShenYu网关中集成ActiveMQ消息队列,帮助开发者构建高性能、可靠的分布式消息传递系统,轻松实现服务间的异步通信与解耦。 ## 为什么选择ShenYu集成ActiveMQ? 在现代微服务架构中,消息队列是实现服务解耦、流量削峰和异步通信的关键组件。Apache Sh

gitblog_00949的博客 865

使用 MATLAB/Simulink 构建一个分布式光伏-储能联合系统在配电网中的协同控制模型

本文介绍了使用MATLAB/Simulink构建分布式光伏-储能联合系统协同控制模型的方法。该系统包含光伏发电、储能、负荷和控制器等模块,通过协同控制实现能量优化管理。建模过程详细说明了从创建项目、搭建组件到实现控制策略的步骤,包括光伏特性模拟、储能参数设置和负荷需求建模。重点设计了协同控制器算法,实时调节电力分配,并展示了系统运行指标和经济效益分析。该方法为多能互补系统在配电网中的应用提供了技术验证,未来可进一步优化控制算法和场景适应性。

MHD0815的博客 302

消息队列就改这么学(ActiveMQ)

ActiveMQ介绍 MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。 MQ的消费-生产者模型的一个典型的代表,一端往消息队列中不断的写入消息,而另一端则可以读取或者订阅队列中的消息。MQ和JMS类似 JMS即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用...

RuiDer的博客 285

Veritas NetBackup8.1.1客户端安装

1.在客户机打开安装包运行Browser 2.选择NetBackup Client Software Installation 3.下一步 4.接受,下一步 5.我选择自定义安装,下一步 6.我选择安装到D盘 7.默认下一步 8.使用域管理账户 9.输入服务器安装了服务端的那一台名称 10.输入服务端生成的令牌,选择“I recognize the fingerprint for this host.Proceed with the certificate deploymen”下一步

万物皆可学 2万+

[QMT和ptrade量化交易小白入门]-109、小市值选股的基础上用六脉神剑评分,回测年化可到132%

本专栏主要是介绍QMT和ptrade的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写200篇左右。QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。

木头大左的博客 1843

Android多线程:HandlerThread学习指南(含具体使用、原理 & 源码分析)

前言 多线程的应用在Android开发中是非常常见的,常用方法主要有: 继承Thread类 实现Runnable接口 Handler HandlerThread AsyncTask 今天,我将献上一份全面 & 详细的HandlerThread机制的学习指南,内容包括:定性认知、定量使用、工作原理 & 源码分析,希望你们会喜欢。 目录 1. 定义 一个Andro...

Carson带你学Android 2934

Yandex复杂还原验证码识别

本文分析了Yandex滑动验证码的识别方法,指出其通过JS判断正确滑动位置而非用户交互还原图像。核心思路是通过像素差异分析,将验证码图像重组后(分为3×3或4×4),计算相邻图像间的像素差异,最小值即为正确位置。与前期某象验证码识别方法类似,需结合图像增强技术统一像素直方图。文章强调该方法仅供学习参考,严禁用于违法行为。完整识别流程包括图像重组、像素差异计算等步骤,最终可准确定位正确拼图位置。

码王吴彦祖的博客 273

RabbitMQ 入门 && RabbitMQ 4.0 核心新特性

RabbitMQ 凭借其灵活的路由策略、低延迟及多协议支持,成为企业级异步通信的首选。若需超高性能或大数据流处理,可结合 Kafka 使用;若需强事务支持,可参考 RocketMQ。具体技术选型需结合业务场景中的。

csdn_tom_168的博客 1348

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

边缘计算节点编程的5大关键技术,你掌握了几项?

掌握工业4.0中的边缘计算节点编程,提升实时响应与设备协同效率。本文详解协议解析、轻量级容器、数据过滤、安全通信与远程运维5大关键技术,覆盖智能制造、预测维护等场景,助力工业系统高效部署,值得收藏。

InstrIsle的博客 930

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

【PHP-Python模型交互实战指南】:掌握跨语言调用的5大核心方案

掌握PHP-Python的模型交互难题,本文详解5大跨语言调用方案,涵盖REST API、消息队列、进程通信等核心方法,适用于Web服务与AI模型集成场景。高效稳定,开发运维更便捷,值得收藏。

LogicNest的博客 465

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

NetMQ完全指南:如何在.NET中构建高性能分布式系统

NetMQ 是一个专为 .NET 平台设计的轻量级消息传递库,它是 ZeroMQ 的 100% 原生 C# 实现。对于想要构建高性能分布式系统的开发者来说,NetMQ 提供了强大的异步消息队列抽象、多种消息传递模式和无代理架构,是替代传统 WCF 或 MSMQ 的理想选择。本文将为您提供完整的 NetMQ 入门指南,帮助您快速掌握这个强大的分布式通信框架。 ## 🚀 为什么选择 NetMQ?

gitblog_00346的博客 1072

【VSCode聊天终端输出访问全攻略】:掌握高效调试与实时交互的5大核心技巧

掌握VSCode聊天终端输出访问技巧,提升开发调试效率。适用于远程协作、实时日志监控与插件交互场景,详解5大核心方法:集成终端使用、输出重定向、聊天API调用、实时数据捕获与扩展插件配置。操作简便,增强开发交互性,值得收藏。

BytePulse的博客 679

如何通过消息队列集成提升agno多智能体系统通信可靠性

在构建高性能多智能体系统时,通信可靠性是确保系统稳定运行的核心挑战。agno作为专注于多智能体系统的高性能运行时框架,其消息传递机制直接影响整体系统的响应速度和容错能力。本文将详细介绍如何通过消息队列集成,为agno多智能体系统打造更可靠、高效的通信架构,帮助开发者解决分布式环境下的消息丢失、延迟和同步问题。 ## 为什么消息队列是多智能体通信的关键 多智能体系统中,智能体间的通信往往面临三大

gitblog_01038的博客 817

Go与RabbitMQ:构建高效消息系统

核心收获:回顾 RabbitMQ 在 Go 中的关键应用场景。扩展学习与其他消息队列对比(如 Kafka, NATS)。高级模式:RPC 调用、延迟队列。实战项目:微服务架构中的消息集成。资源推荐:官方文档、社区工具、示例仓库链接。这个大纲确保了从理论到实践的平滑过渡,每个部分都包含可操作的代码和解释。最终文章可以基于此扩展,添加更多细节和完整代码示例。如需进一步细化某个部分,请随时告知!

lz989796的博客 814

揭秘Python分布式任务调度:如何用Celery构建可扩展的微服务系统

掌握Python分布式系统开发核心技巧,揭秘如何用Celery实现高效任务调度与微服务解耦。适用于高并发场景下的异步处理、定时任务与系统扩展,提升性能与可靠性。构建可扩展系统的实用指南,值得收藏。

CodeWhim的博客 966

智能金融交易系统搭建:基于多智能体协作框架的实现指南

### 1.1 系统总体架构 **智能交易系统**采用多智能体协作框架,通过模块化设计实现市场数据处理、分析决策和交易执行的全流程自动化。系统架构遵循分层设计原则,从数据接入层到决策执行层形成完整闭环,各模块间通过标准化接口实现松耦合通信。 智能交易系统架构图 > [!NOTE] > **核心架构特点**:采用事件驱动模型,支持实时数据流处理;智能体间通过消息队列实现异步通信;决策过程采用加

gitblog_00086的博客 456

边缘节点间通信延迟高?PHP开发者必备的4种低延迟解决方案

解决边缘计算中网络通信延迟难题,PHP开发者可借助本地缓存、异步通信、轻量协议与边缘服务编排4种方案,显著提升边缘节点间响应速度。适用于物联网、实时应用等场景,实现高效低延迟交互,值得收藏。

IterStream的博客 339

Java游戏服务器架构如何扛住百万并发?:从零构建高性能后端系统的5大核心原则

掌握Java游戏后端架构设计核心原则,轻松应对百万并发挑战。本文详解高可用、低延迟服务器搭建方法,涵盖异步处理、分布式部署与性能优化等关键技术,适用于大型在线游戏场景,值得收藏。

AlgoInk的博客 436

【PHP边缘计算实战指南】:掌握高效网络通信的5大核心技术

掌握PHP边缘计算中的高效网络通信方法,解决分布式场景下数据延迟与性能瓶颈。涵盖轻量级消息队列、Swoole协程通信、API网关优化、数据序列化策略及边缘节点同步机制五大核心技术。适用于物联网、实时应用等高并发场景,提升响应速度与系统稳定性,值得收藏。

PixelFlow的博客 728

HandlerThread源码分析

1什么是HandlerThread? 1.轻量级异步通信类 2.一个实现了Handler通信机制的线程 继承Thread 3.HandlerThread内部实现了Looper对象,可以进行Looper循环 2 使用步骤 // 步骤1:创建HandlerThread实例对象 // 传入参数 = 线程名字,作用 = 标记该线程 HandlerThread mHandlerThread = new...

lin_henry的博客 252
上一篇: C#与ONNX Runtime实现工业视觉缺陷检测的高效方案
下一篇: Linux系统iowait异常监控与eBPF解决方案
weixin_34186128
博客等级 码龄11年 7154粉丝 1147原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值