UnigineEditor
Interface Overview
Assets Workflow
Settings and Preferences
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Landscape Tool
Using Editor Tools for Specific Tasks
FAQ
Programming
Setting Up Development Environment
Usage Examples
UnigineScript
C++
C#
UUSL (Unified UNIGINE Shader Language)
File Formats
Rebuilding the Engine and Tools
GUI
Double Precision Coordinates
API
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
CIGI Client Plugin
Rendering-Related Classes

Thread Safety in API

Unigine API objects are guaranteed to be used safely in the main loop. When it comes to multiple user threads, things get a bit more complicated.

As not all of API classes are thread-safe, you should take into consideration the behaviour type of an API member to reach safe multithreading in your application.

All types require special approaches described in this article.

See Also#

Dealing with Multiple Threads#

Thread-Safe Objects#

Fully thread-safe objects are free to use in any thread, be it the main loop or a user's one. This is provided due to thread synchronization mechanisms making all critical operations atomic and protecting data structures, it negates issues like race condition and others.

Only one thread is allowed to access data at the same time, while data is locked for other threads, that is why multiple threads may have to wait for each other until their tasks are finished.

Notice
Many requests to data from multiple threads may cause additional performance loss due to synchronization.

The following API members are considered to be thread-safe:

Loading Nodes Asynchronously#

It is not allowed to load nodes in user threads. For that purpose the AsyncQueue Class is recommended to be used.

AppWorldLogic.h

Source code (UnigineScript)
#ifndef __APP_WORLD_LOGIC_H__
#define __APP_WORLD_LOGIC_H__

#include <UnigineLogic.h>
#include <UnigineStreams.h>
#include <UnigineThread.h>

using namespace Unigine;
using namespace Math;

class AppWorldLogic : public Unigine::WorldLogic {
	
public:
	AppWorldLogic() {}
	virtual ~AppWorldLogic() {}
	
	virtual int init();
	virtual int shutdown();

private:
	Thread *thread1;
	Thread *thread2;
};

#endif // __APP_WORLD_LOGIC_H__

AppWorldLogic.cpp

Source code (UnigineScript)
#include "AppWorldLogic.h"
#include <UnigineAsyncQueue.h>

class MeshProducerThread : public Thread
{
public:
	MeshProducerThread()
	{
		callback = AsyncQueue::get()->addCallback(AsyncQueue::CALLBACK_MESH_LOADED, MakeCallback(this, &MeshProducerThread::mesh_loaded));
	}
	~MeshProducerThread()
	{
		AsyncQueue::get()->removeCallback(AsyncQueue::CALLBACK_MESH_LOADED, callback);
	}

public:
	void process()
	{
		while (isRunning())
		{
			mesh_id = AsyncQueue::get()->loadMesh("core\\meshes\\material_ball.mesh");
			wait();
			Log::message("Thread %d: mesh loaded\n", getID());
			AsyncQueue::get()->takeMesh(mesh_id);
		}
	}

private:
	void mesh_loaded(const char *name, int id)
	{
		if (mesh_id == id)
			signal();
	}

private:
	int mesh_id = 0;
	int callback = 0;
};

int AppWorldLogic::init()
{
	thread1 = new MeshProducerThread();
	thread1->run();
	thread2 = new MeshProducerThread();
	thread2->run();

	return 1;
}

int AppWorldLogic::shutdown()
{
	thread1->stop();
	thread2->stop();

	return 1;
}

Avoiding Deadlocks#

There is a posibility of mutual locking, also known as deadlock, on condition that a function of a locked object executes a callback function which in turn calls a function of the same locked object.

Notice
To prevent deadlocks you should avoid calling potentially locked objects from their callback functions.

Intersections with Global Terrain#

ObjectTerrainGlobal contains a set of thread-safe methods intended for some special use cases.

AppWorldLogic.h

Source code (C++)
#ifndef __APP_WORLD_LOGIC_H__
#define __APP_WORLD_LOGIC_H__

#include <UnigineLogic.h>
#include <UnigineStreams.h>
#include <UnigineThread.h>
#include <UnigineVector.h>

using namespace Unigine;
using namespace Math;

class AppWorldLogic : public Unigine::WorldLogic {

public:
	AppWorldLogic();
	virtual ~AppWorldLogic();
	
	virtual int init();
	virtual int shutdown();

private:
	Unigine::Vector<Thread*> threads;
};

#endif // __APP_WORLD_LOGIC_H__

AppWorldLogic.cpp

Source code (C++)
#include "AppWorldLogic.h"

#include <UnigineEditor.h>
#include <UnigineObjects.h>
#include <UnigineGame.h>

class TerrainIntersectionThread : public Thread
{
public:
	TerrainIntersectionThread(ObjectTerrainGlobalPtr terrain_)
	{
		terrain = terrain_;
		intersection = ObjectIntersection::create();
	}

	void process() override
	{
		while (isRunning())
		{
			float x = Game::get()->getRandomFloat(-1000.0f, 1000.0f);
			float y = Game::get()->getRandomFloat(-1000.0f, 1000.0f);

			int success = terrain->getIntersection(vec3{ x, y, 10000.0f }, vec3{ x, y, 0.0 }, intersection, 0);
			if (success)
			{
				const auto intersection_point = intersection->getPoint();
				Log::message("Thread %d: %f %f %f\n", getID(), intersection_point.x, intersection_point.y, intersection_point.z);
			}
		}
	}

private:
	ObjectTerrainGlobalPtr terrain;
	ObjectIntersectionPtr intersection;
};

AppWorldLogic::AppWorldLogic()
{
}

AppWorldLogic::~AppWorldLogic()
{
}

int AppWorldLogic::init()
{
	const auto terrain = ObjectTerrainGlobal::cast(Editor::get()->getNodeByName("Landscape"));

	int num_thread = 4;
	for (int i = 0; i < num_thread; ++i)
	{
		Thread *thread = new TerrainIntersectionThread(terrain);
		thread->run();
		threads.push_back(thread);
	}

	return 1;
}

int AppWorldLogic::shutdown()
{
	for (Thread *thread : threads)
	{
		thread->stop();
		delete thread;
	}

	return 1;
}

Main-Loop-Dependent Objects#

The Node class and the Node-Related classes are directly involved into threads of the main loop. They don't have synchronization mechanisms provided.

To safely operate on these objects from user threads, you should firstly pause the main loop to avoid interference. Then, you can run a required number of jobs for processing of nodes. After all jobs are done, continue the main loop.

Notice
Gpu-related methods must be called only in the main loop.

For some typical cases it is recommended to use the following objects:

Main-Loop-Independent Objects#

There are also API members which are not involved into the main loop, they don't have synchronization algorythms as well.

You can fully manage such an object in any thread, but please note that if you need to send it to another thread, either the main loop or a user's thread, you have to provide manual synchronization for its data consistency.

For this purpose you are free to use the following methods provided in the Unigine namespace:

  • SpinLock method which causes a thread waiting in a loop until the lock is available.
  • WaitLock method.
  • AtomicCAS function presents atomic compare and swap.

The following API members are considered to be independent of the main loop threads:

Below you'll find the C++ implementation of an example of manual synchronization using the AtomicLock class and the SpinLock method.

Thread.cpp

Source code (C++)
#include <UnigineApp.h>
#include <UnigineConsole.h>
#include <UnigineEngine.h>
#include <UnigineLogic.h>
#include <UnigineThread.h>

using namespace Unigine;

class MyThread : public Thread
{
public:
	MyThread()
		: lock(0)
		, value(0)
	{
	}

	void setValue(int v)
	{
		AtomicLock atomic(&lock);
		value = v;
	}

	int getValue() const
	{
		AtomicLock atomic(&lock);
		return value;
	}

protected:
	void process()
	{
		while (isRunning())
		{
			SpinLock(&lock, 0, 1);
			Log::warning("Hello from C++ thread %d\n", value++);
			SpinLock(&lock, 1, 0);

			usleep(1000000);
		}
	}

private:
	mutable volatile int lock;

	int value;
};


class AppSystemLogic : public SystemLogic
{
public:
	AppSystemLogic() {}
	virtual ~AppSystemLogic() {}
	virtual int init()
	{
		App::get()->setUpdate(1);
		Console::get()->setActivity(1);
		return 1;
	}
};


int main(int argc, char **argv)
{
	// create thread
	MyThread *thread = new MyThread();

	// init engine
	EnginePtr engine(UNIGINE_VERSION, argc, argv);

	// run thread
	thread->run();

	// set thread value
	thread->setValue(13);

	// enter main loop
	AppSystemLogic system_logic;
	engine->main(&system_logic, NULL, NULL);

	// terminate thread
	thread->terminate();

	// delete thread
	delete thread;

	return 0;
}

GPU-Related Objects#

Some members methods interact with Graphics API, which is available only in the main loop. Once you need to call a gpu-related function, you have to pass the object to the main loop and perform calling in it.

The Rendering-Related Classes (e.g. MeshDynamic) should be considered as gpu-related.

Also, the Object-Related Classes have rendering-related methods, such as render() and other ones.

Notice
Note that rendering-related methods should be called only from callback functions (see an example of creating a callback function).

Below you'll find the source code of the dynamic_03 default sample which demonstrates how to create a dynamic mesh by using the Marching cubes algorithm performed asynchronously.

dynamic_03.usc

Source code (UnigineScript)
#include <core/scripts/samples.h>
#include <samples/objects/dynamic_01.h>

/*
 */
Async async_0;
Async async_1;
int size = 32;
float field_0[size * size * size];
float field_1[size * size * size];
int flags_0[size * size * size];
int flags_1[size * size * size];
ObjectMeshDynamic mesh_0;
ObjectMeshDynamic mesh_1;

using Unigine::Samples;

/*
 */
string mesh_material_names[] = ( "objects_mesh_red", "objects_mesh_green", "objects_mesh_blue", "objects_mesh_orange", "objects_mesh_yellow" );

string get_mesh_material(int material) {
	return mesh_material_names[abs(material) % mesh_material_names.size()];
}

/*
 */
void update_thread() {
	
	while(1) {
		
		float time = engine.game.getTime();
		
		// wait async
		if(async_1 == NULL) async_1 = new Async();
		while(async_1 != NULL && async_1.isRunning()) wait;
		if(async_1 == NULL) continue;
		async_1.clearResult();
		
		// copy mesh
		Mesh mesh = new Mesh();
		mesh_1.getMesh(mesh);
		mesh_0.setMesh(mesh);
		mesh_0.setMaterial(get_mesh_material(1),"*");
		delete mesh;
		
		// wait async
		if(async_0 == NULL) async_0 = new Async();
		while(async_0 != NULL && async_0.isRunning()) wait;
		if(async_0 == NULL) continue;
		async_0.clearResult();
		
		// swap buffers
		field_1.swap(field_0);
		flags_1.swap(flags_0);
		
		// create field
		float angle = sin(time) + 3.0f;
		mat4 transform = rotateZ(time * 25.0f) * scale(vec3(5.0f / size)) * translate(vec3(-size / 2.0f));
		async_0.run(functionid(create_field),field_0.id(),flags_0.id(),size,transform,angle);
		
		// create mesh
		async_1.run(functionid(marching_cubes),mesh_1,field_1.id(),flags_1.id(),size);
		
		wait;
	}
}

/*
 */
int init() {
	
	createInterface("samples/objects/dynamic_03.world");
	engine.render.loadSettings(fullPath("samples/common/world/render.render"));
	createDefaultPlayer(Vec3(30.0f,0.0f,20.0f));
	createDefaultPlane();
	
	mesh_0 = addToEditor(new ObjectMeshDynamic(OBJECT_DYNAMIC_ALL));
	mesh_0.setWorldTransform(Mat4(scale(vec3(16.0f / size)) * translate(-size / 2.0f,-size / 2.0f,0.0f)));
	
	mesh_1 = new ObjectMeshDynamic(1);
	mesh_1.setEnabled(0);
	
	setDescription(format("Async dynamic marching cubes on %dx%dx%d grid",size,size,size));
	
	thread("update_thread");
	
	return 1;
}

/*
 */
void shutdown() {
	
	if(async_0 != NULL) async_0.wait();
	if(async_1 != NULL) async_1.wait();
	return 1;
}

Threads in UnigineScript#

When using UnigineScript workflow, you should also keep in mind that main-loop-dependent objects must not be directly modified out of the main loop. Instead, it is suggested to create a twin for such an object which will be modyfied asynchronously and then swapped with the original object on the flush step.

Notice
Non-reentrant UnigineScript functions are not suitable for multithreaded usage. You will have to create a separate function for each thread. For that you can use Templates.

Below you'll find a UnigineScript sample on managing several mesh clusters asynchronously. You can copy and paste it to the world script file of your project.

cluster_03.usc

Source code (UnigineScript)
#include <core/unigine.h>
#include <core/scripts/samples.h>

using Unigine::Samples;

#define NUM_CLUSTERS 4
int size = 60;

// a class for asynchronous mesh cluster
class AsyncCluster
{
	public:
	Mat4 transforms[0];
	// original mesh cluster
	ObjectMeshCluster cluster;
	// a twin for async modification
	ObjectMeshCluster cluster_async;
	Async async;
};
AsyncCluster clusters[NUM_CLUSTERS];

string mesh_material_names[] = ( "stress_mesh_red", "stress_mesh_green", "stress_mesh_blue", "stress_mesh_orange", "stress_mesh_yellow" );

string get_mesh_material(int material) {
	return mesh_material_names[abs(material) % mesh_material_names.size()];
}

// a template to generate a function transforming a cluster in each thread
template async_transforms<NUM, OFFSET_X, OFFSET_Y> void async_transforms_ ## NUM(ObjectMeshCluster cluster_async, float transforms[], float time, int size) {
	
	Vec3 offset = Vec3(OFFSET_X - 0.5f, OFFSET_Y - 0.5f, 0.0f) * (size + 0.5f) * 2;
	
	int num = 0;
	for(int y = -size; y <= size; y++) {
		for(int x = -size; x <= size; x++) {
			float rand = sin(frac(num * 0.333f) + x * y * (NUM + 1));
			
			Vec3 pos = (Vec3(x, y, sin(time * rand * 2.0f) + 1.5f) + offset) * 2.0f;
			transforms[num] = translate(pos) * rotateZ(time * 25 * rand);
			num++;
		}
	}
	
	cluster_async.createMeshes(transforms);
}

async_transforms<0,0,0>;
async_transforms<1,0,1>;
async_transforms<2,1,0>;
async_transforms<3,1,1>;

void update_thread() {
	
	while(1) {
		
		// wait async
		for(int i = 0; i < NUM_CLUSTERS; i++) {
			while(clusters[i].async.isRunning())
				wait;
		}
		
		for(int i = 0; i < NUM_CLUSTERS; i++) {
			AsyncCluster c = clusters[i];
			
			c.async.clearResult();
			c.cluster.swap(c.cluster_async);
			c.cluster.setEnabled(1);
			c.cluster_async.setEnabled(0);
			c.async.run("async_transforms_" + i, c.cluster_async, c.transforms.id(), engine.game.getTime(), size);
		}
		
		wait;
	}
}

int init() {
	// create scene
	PlayerSpectator player = new PlayerSpectator();
	player.setPosition(Vec3(30.0f,0.0f,20.0f));
	player.setDirection(vec3(-1.0f, 0.0f, -0.5f));
	engine.game.setPlayer(player);
	
	for(int i = 0; i < NUM_CLUSTERS; i++) {
		AsyncCluster c = new AsyncCluster();
		c.cluster = new ObjectMeshCluster(fullPath("samples/common/meshes/box.mesh"));
		c.cluster.setMaterial(get_mesh_material(i),"*");
		c.cluster_async = class_append(node_cast(c.cluster.clone()));
		c.async = new Async();
		int num = pow(size * 2 + 1, 2);
		c.transforms.resize(num);
		clusters[i] = c;
	}
	
	thread("update_thread");
	
	int num = pow(size * 2 + 1, 2) * NUM_CLUSTERS;
	log.message("ObjectMeshCluster with %d dynamic instances",num);
	
	return 1;
}

/*
 */
void shutdown() {
	
	for(int i = 0; i < NUM_CLUSTERS; i++) {
		clusters[i].async.wait();
	}
	
	return 1;
}
Last update: 2019-04-30