This page has been translated automatically.
Видеоуроки
Интерфейс
Основы
Продвинутый уровень
Подсказки и советы
Основы
Программирование на C#
Рендеринг
Профессиональный уровень (SIM)
Принципы работы
Свойства (properties)
Компонентная Система
Рендер
Физика
Редактор UnigineEditor
Обзор интерфейса
Работа с ассетами
Контроль версий
Настройки и предпочтения
Работа с проектами
Настройка параметров ноды
Setting Up Materials
Настройка свойств
Освещение
Sandworm
Использование инструментов редактора для конкретных задач
Расширение функционала редактора
Встроенные объекты
Ноды (Nodes)
Объекты (Objects)
Эффекты
Декали
Источники света
Geodetics
World-ноды
Звуковые объекты
Объекты поиска пути
Player-ноды
Программирование
Основы
Настройка среды разработки
Примеры использования
C++
C#
UnigineScript
UUSL (Unified UNIGINE Shader Language)
Плагины
Форматы файлов
Материалы и шейдеры
Rebuilding the Engine Tools
Интерфейс пользователя (GUI)
Двойная точность координат
API
Animations-Related Classes
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
VR-Related Classes
Работа с контентом
Оптимизация контента
Материалы
Визуальный редактор материалов
Material Nodes Library
Miscellaneous
Input
Math
Matrix
Textures
Art Samples
Учебные материалы

Потокобезопасность в API

Объекты Unigine API гарантированно безопасно используются в основном цикле. Когда дело доходит до нескольких пользовательских потоков, все становится немного сложнее.

Поскольку не все классы API являются потокобезопасными, вы должны учитывать тип поведения члена API, чтобы обеспечить безопасную многопоточность в приложении.

Все типы требуют особых подходов, описанных в этой статье.

Смотрите также#

Работа с несколькими потоками#

Потокобезопасные объекты#

Полностью потокобезопасные объекты можно использовать в любом потоке, будь то основной или пользовательский цикл. Это обеспечивается за счет того, что механизмы синхронизации потоков делают все критические операции атомарными и защищают структуры данных, они устраняют такие проблемы, как "состояние гонки" и другие.

Только одному потоку разрешен доступ к данным одновременно, в то время как для других потоков данные заблокированы, поэтому нескольким потокам, возможно, придется ждать друг друга, пока их задачи не будут завершены.

Примечание
Множество запросов к данным из нескольких потоков могут вызвать дополнительную потерю производительности из-за синхронизации.

Следующие члены API считаются потокобезопасными:

Как избежать взаимных блокировок#

Существует вероятность взаимной блокировки, также известной как deadlock, если функция заблокированного объекта выполняет функцию обратного вызова (callback), которая, в свою очередь, вызывает функцию того же заблокированного объекта.

Примечание
Чтобы предотвратить взаимоблокировки, вам следует избегать вызова потенциально заблокированных объектов из их функций обратного вызова.

Операции с ландшафтом#

Классы ObjectLandscapeTerrain содержат набор потокобезопасных методов, предназначенных для получения данных ландшафта и обнаружения пересечений.

AppWorldLogic.h

Исходный код (C++)
#include <UnigineLogic.h>
#include <UnigineStreams.h>
#include <UnigineThread.h>

class AppWorldLogic : public Unigine::WorldLogic
{
public:
	AppWorldLogic();
	~AppWorldLogic() override;

	int init() override;
	int shutdown() override;
private:

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

};

AppWorldLogic.cpp

Исходный код (C++)
#include "AppWorldLogic.h"
#include <UniginePlayers.h>
#include <UnigineGame.h>
#include <UnigineVisualizer.h>
#include <UnigineObjects.h>
#include <UnigineWorld.h>

using namespace Unigine;
using namespace Math;

class TerrainIntersectionThread : public Thread
{
public:
	TerrainIntersectionThread(PlayerPtr m)
	{
		main_player = m;
	}

	void process() override
	{
		if (!main_player)
			return;
		while (isRunning())
		{

			float x = Game::getRandomFloat(-1000.0f, 1000.0f);
			float y = Game::getRandomFloat(-1000.0f, 1000.0f);

			if (!fetch)
			{
				// create fetch
				fetch = LandscapeFetch::create();

				// set mask
				fetch->setUsesHeight(true);
				fetch->setUsesNormal(true);
				fetch->setUsesAlbedo(true);
				fetch->setUsesMask(0, true);
				fetch->setUsesMask(1, true);
				fetch->setUsesMask(2, true);
				fetch->setUsesMask(3, true);

				fetch->intersectionAsync(Vec3{ x, y, 10000.0f }, Vec3{ x, y, 0.0 }, false);
			}
			else
			{
				if (fetch->isAsyncCompleted())
				{
					if (fetch->isIntersection())
					{
						Vec3 point = fetch->getPosition();
						Visualizer::renderVector(point, point + Vec3_up * 10, vec4_blue);
						Visualizer::renderVector(point, point + Vec3(fetch->getNormal() * 10), vec4_red);
						Visualizer::renderSolidSphere(1, translate(point), vec4_black);

						String string;
						string += String::format("Height : %f\n", fetch->getHeight());

						string += "Masks: \n";

						auto terrain = Landscape::getActiveTerrain();
						for (int i = 0; i < 4; i++)
						{
							// getName() is not thread-safe,
							// do not change the mask name in other threads when getting
							string += String::format(" - \"%s\": %.2f\n", terrain->getDetailMask(i)->getName(), fetch->getMask(i));
						}
						Visualizer::renderMessage3D(point, vec3(1, 1, 0), string.get(), vec4_green, 1);
					}
					else
					{
						Visualizer::renderMessage3D(Vec3(x, y, 0), vec3(1, 1, 0), "Out of terrain", vec4_red, 1);
					}

					fetch->intersectionAsync(Vec3{ x, y, 10000.0f }, Vec3{ x, y, 0.0 }, false);
				}
			}
		}
	}

private:
	LandscapeFetchPtr fetch;
	PlayerPtr main_player;
};

int AppWorldLogic::init()
{

	PlayerPtr main_player = checked_ptr_cast<Player>(World::getNodeByName("main_player"));

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

	return 1;
}

int AppWorldLogic::shutdown()
{

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

	return 1;
}

Пересечения с Global Terrain#

ObjectTerrainGlobal содержит набор потокобезопасных методов, предназначенных для некоторых особых случаев использования.

AppWorldLogic.h

Исходный код (C++)
#include <UnigineLogic.h>
#include <UnigineStreams.h>
#include <UnigineThread.h>

class AppWorldLogic : public Unigine::WorldLogic
{
public:
	AppWorldLogic();
	~AppWorldLogic() override;

	int init() override;
	int shutdown() override;
private:

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

};

AppWorldLogic.cpp

Исходный код (C++)
#include "AppWorldLogic.h"
#include <UniginePlayers.h>
#include <UnigineGame.h>
#include <UnigineVisualizer.h>
#include <UnigineObjects.h>
#include <UnigineWorld.h>

using namespace Unigine;
using namespace Math;

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

	void process() override
	{
		while (isRunning())
		{
			float x = Game::getRandomFloat(-1000.0f, 1000.0f);
			float y = Game::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;
};

int AppWorldLogic::init()
{

	const auto terrain = checked_ptr_cast<ObjectTerrainGlobal>(World::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;
}

Объекты, зависящие от основного цикла#

Класс Node и классы, связанные с Node, напрямую участвуют в потоках основного цикла. У них нет механизмов синхронизации.

Чтобы безопасно работать с этими объектами из пользовательских потоков, вы должны сначала приостановить основной цикл, чтобы избежать помех. Затем можно запустить необходимое количество задач для обработки нод. После того, как все задачи будут выполнены, продолжите основной цикл.

Примечание
Методы, связанные с графическим процессором, должны вызываться только в основном цикле.

Безопасность потоков обеспечивается синхронизацией всех потоков Engine, работающих с объектами, зависящими от основного цикла, с Engine::swap(), где выполняется отложенное удаление объектов. Но пользовательские потоки могут выполняться параллельно с Engine::swap() в основном потоке, в таких случаях вам не следует выполнять какие-либо манипуляции с объектами, зависящими от основного цикла (такими как ноды) во время Engine::swap().

Для некоторых типичных случаев рекомендуется использовать следующие объекты:

  • Класс AsyncQueue предназначен для асинхронной загрузки ресурсов.
  • Класс Async предназначен для асинхронного выполнения пользовательских задач.

Объекты, не зависящие от основного цикла#

Есть также члены API, которые не задействованы в основном цикле, у них также нет алгоритмов синхронизации.

Вы можете полностью управлять таким объектом в любом потоке, но обратите внимание, что если вам нужно отправить его в другой поток, либо в основной цикл, либо в поток пользователя, вы должны обеспечить ручную синхронизацию для согласованности его данных.

Для этой цели вы можете использовать любые методы и классы, содержащиеся в файле include/UnigineThread.h, или другие механизмы по своему усмотрению.

Следующие члены API считаются независимыми от потоков основного цикла:

См. Thread C++ Sample для реализации ручной синхронизации с использованием ScopedLock на основе простого мьютекса (Mutex) на C++.

Объекты, связанные с GPU#

Некоторые методы-члены взаимодействуют с Graphics API, который доступен только в основном цикле. Как только вам нужно вызвать функцию, связанную с графическим процессором, вы должны передать объект в основной цикл и выполнить в нем вызов.

Классы, связанные с рендерингом (например, MeshDynamic), следует рассматривать как связанные с графическим процессором.

Кроме того, классы, относящиеся к объектам, имеют методы, связанные с рендерингом, такие как render() и другие.

Примечание
Обратите внимание, что методы, связанные с рендерингом, должны вызываться только из функций-обработчиков событий Render/Viewport (см. пример создания такой функции.

Ниже вы найдете исходный код примера dynamic_03, который демонстрирует, как создать динамический меш с использованием алгоритма Marching Cubes, выполняемого асинхронно.

dynamic_03.usc

Исходный код (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;
}

Потоки в UnigineScript#

При использовании рабочего процесса UnigineScript вы также должны помнить, что объекты, зависящие от основного цикла, не должны изменяться напрямую вне основного цикла. Вместо этого предлагается создать двойника для такого объекта, который будет асинхронно изменен, а затем заменен на исходный объект на шаге flush.

Примечание
Функции UnigineScript без повторного входа не подходят для многопоточного использования. Вам нужно будет создать отдельную функцию для каждого потока. Для этого вы можете использовать Шаблоны.

Ниже вы найдете пример UnigineScript по асинхронному управлению несколькими Mesh Cluster. Вы можете скопировать и вставить его в файл сценария мира вашего проекта.

cluster_03.usc

Исходный код (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;
}
Последнее обновление: 18.01.2024
Build: ()