Multithreading is a powerful technique that allows a program to perform multiple tasks concurrently, improving performance and responsiveness. In Unreal Engine, multithreading can be used to handle complex computations, AI processing, and other tasks without blocking the main game thread. This section will cover the basics of multithreading in Unreal Engine, including practical examples and exercises to help you understand and implement multithreading in your projects.

Key Concepts

  1. Threads: The smallest unit of processing that can be scheduled by an operating system.
  2. Concurrency: The ability to run multiple tasks simultaneously.
  3. Parallelism: The actual simultaneous execution of multiple tasks.
  4. Thread Safety: Ensuring that shared data is accessed by only one thread at a time to prevent data corruption.

Setting Up Multithreading in Unreal Engine

Creating a Simple Thread

To create a thread in Unreal Engine, you need to define a class that implements the FRunnable interface. This interface requires you to implement several methods, including Init(), Run(), and Stop().

// MyRunnable.h
#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"

class MYPROJECT_API FMyRunnable : public FRunnable
{
public:
    FMyRunnable();
    virtual ~FMyRunnable();

    virtual bool Init() override;
    virtual uint32 Run() override;
    virtual void Stop() override;

private:
    FThreadSafeCounter StopTaskCounter;
};

// MyRunnable.cpp
#include "MyRunnable.h"

FMyRunnable::FMyRunnable() {}

FMyRunnable::~FMyRunnable() {}

bool FMyRunnable::Init()
{
    // Initialization code here
    return true;
}

uint32 FMyRunnable::Run()
{
    while (StopTaskCounter.GetValue() == 0)
    {
        // Task execution code here
    }
    return 0;
}

void FMyRunnable::Stop()
{
    StopTaskCounter.Increment();
}

Starting and Stopping the Thread

To start and stop the thread, you need to use the FRunnableThread class.

// MyThreadManager.h
#pragma once

#include "CoreMinimal.h"
#include "MyRunnable.h"

class MYPROJECT_API FMyThreadManager
{
public:
    FMyThreadManager();
    ~FMyThreadManager();

    void StartThread();
    void StopThread();

private:
    FRunnableThread* Thread;
    FMyRunnable* Runnable;
};

// MyThreadManager.cpp
#include "MyThreadManager.h"

FMyThreadManager::FMyThreadManager()
    : Thread(nullptr), Runnable(nullptr)
{
}

FMyThreadManager::~FMyThreadManager()
{
    StopThread();
}

void FMyThreadManager::StartThread()
{
    if (!Runnable && !Thread)
    {
        Runnable = new FMyRunnable();
        Thread = FRunnableThread::Create(Runnable, TEXT("MyThread"));
    }
}

void FMyThreadManager::StopThread()
{
    if (Runnable && Thread)
    {
        Runnable->Stop();
        Thread->WaitForCompletion();
        delete Thread;
        delete Runnable;
        Thread = nullptr;
        Runnable = nullptr;
    }
}

Practical Example: Background Data Processing

Let's create a practical example where we process some data in the background without blocking the main game thread.

// DataProcessor.h
#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"

class MYPROJECT_API FDataProcessor : public FRunnable
{
public:
    FDataProcessor(TArray<int32> InData);
    virtual ~FDataProcessor();

    virtual bool Init() override;
    virtual uint32 Run() override;
    virtual void Stop() override;

private:
    TArray<int32> Data;
    FThreadSafeCounter StopTaskCounter;
};

// DataProcessor.cpp
#include "DataProcessor.h"

FDataProcessor::FDataProcessor(TArray<int32> InData)
    : Data(InData)
{
}

FDataProcessor::~FDataProcessor() {}

bool FDataProcessor::Init()
{
    // Initialization code here
    return true;
}

uint32 FDataProcessor::Run()
{
    while (StopTaskCounter.GetValue() == 0)
    {
        for (int32& Value : Data)
        {
            Value *= 2; // Example processing: doubling each value
        }
    }
    return 0;
}

void FDataProcessor::Stop()
{
    StopTaskCounter.Increment();
}

Starting the Data Processor

// DataProcessorManager.h
#pragma once

#include "CoreMinimal.h"
#include "DataProcessor.h"

class MYPROJECT_API FDataProcessorManager
{
public:
    FDataProcessorManager();
    ~FDataProcessorManager();

    void StartProcessing(TArray<int32> Data);
    void StopProcessing();

private:
    FRunnableThread* Thread;
    FDataProcessor* Processor;
};

// DataProcessorManager.cpp
#include "DataProcessorManager.h"

FDataProcessorManager::FDataProcessorManager()
    : Thread(nullptr), Processor(nullptr)
{
}

FDataProcessorManager::~FDataProcessorManager()
{
    StopProcessing();
}

void FDataProcessorManager::StartProcessing(TArray<int32> Data)
{
    if (!Processor && !Thread)
    {
        Processor = new FDataProcessor(Data);
        Thread = FRunnableThread::Create(Processor, TEXT("DataProcessorThread"));
    }
}

void FDataProcessorManager::StopProcessing()
{
    if (Processor && Thread)
    {
        Processor->Stop();
        Thread->WaitForCompletion();
        delete Thread;
        delete Processor;
        Thread = nullptr;
        Processor = nullptr;
    }
}

Practical Exercises

Exercise 1: Implement a Simple Timer

Create a thread that acts as a timer, counting seconds in the background and printing the elapsed time to the console.

Solution:

// TimerRunnable.h
#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"

class MYPROJECT_API FTimerRunnable : public FRunnable
{
public:
    FTimerRunnable();
    virtual ~FTimerRunnable();

    virtual bool Init() override;
    virtual uint32 Run() override;
    virtual void Stop() override;

private:
    FThreadSafeCounter StopTaskCounter;
};

// TimerRunnable.cpp
#include "TimerRunnable.h"
#include "HAL/PlatformProcess.h"

FTimerRunnable::FTimerRunnable() {}

FTimerRunnable::~FTimerRunnable() {}

bool FTimerRunnable::Init()
{
    // Initialization code here
    return true;
}

uint32 FTimerRunnable::Run()
{
    int32 Seconds = 0;
    while (StopTaskCounter.GetValue() == 0)
    {
        FPlatformProcess::Sleep(1.0f);
        Seconds++;
        UE_LOG(LogTemp, Log, TEXT("Elapsed Time: %d seconds"), Seconds);
    }
    return 0;
}

void FTimerRunnable::Stop()
{
    StopTaskCounter.Increment();
}

Exercise 2: Data Processing with Progress Reporting

Modify the FDataProcessor class to report progress to the main thread.

Solution:

// DataProcessor.h
#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"

DECLARE_DELEGATE_OneParam(FOnProgressUpdated, float);

class MYPROJECT_API FDataProcessor : public FRunnable
{
public:
    FDataProcessor(TArray<int32> InData, FOnProgressUpdated InProgressDelegate);
    virtual ~FDataProcessor();

    virtual bool Init() override;
    virtual uint32 Run() override;
    virtual void Stop() override;

private:
    TArray<int32> Data;
    FThreadSafeCounter StopTaskCounter;
    FOnProgressUpdated ProgressDelegate;
};

// DataProcessor.cpp
#include "DataProcessor.h"

FDataProcessor::FDataProcessor(TArray<int32> InData, FOnProgressUpdated InProgressDelegate)
    : Data(InData), ProgressDelegate(InProgressDelegate)
{
}

FDataProcessor::~FDataProcessor() {}

bool FDataProcessor::Init()
{
    // Initialization code here
    return true;
}

uint32 FDataProcessor::Run()
{
    int32 TotalItems = Data.Num();
    for (int32 Index = 0; Index < TotalItems && StopTaskCounter.GetValue() == 0; ++Index)
    {
        Data[Index] *= 2; // Example processing: doubling each value
        float Progress = (Index + 1) / static_cast<float>(TotalItems);
        ProgressDelegate.ExecuteIfBound(Progress);
    }
    return 0;
}

void FDataProcessor::Stop()
{
    StopTaskCounter.Increment();
}

Summary

In this section, you learned about multithreading in Unreal Engine, including how to create and manage threads using the FRunnable interface and FRunnableThread class. You also saw practical examples and exercises to help you implement multithreading in your projects. By understanding and utilizing multithreading, you can significantly improve the performance and responsiveness of your Unreal Engine applications.

© Copyright 2024. All rights reserved