API中的线程安全
保证在主循环中安全地使用Unigine API对象。 当涉及到多个用户线程时,事情变得更加复杂。
由于并非所有的API类都是线程安全的,因此应考虑API成员的行为类型,以在应用程序中实现安全的多线程处理。
所有类型都需要本文介绍的特殊方法。
也可以看看#
处理多个线程#
线程安全对象#
完全线程安全的对象可以在任何线程中自由使用,无论是主循环还是用户循环。之所以提供此功能,是因为线程同步机制使所有关键操作变得原子化并保护了数据结构,从而消除了竞赛条件等问题。
仅允许一个线程同时访问数据,而为其他线程锁定数据,这就是为什么多个线程可能不得不互相等待直到其任务完成。
以下API成员被认为是线程安全的:
异步加载节点#
不允许在用户线程中加载节点。为此,建议使用 AsyncQueue类。
AppWorldLogic.h
#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::Thread* thread1;
Unigine::Thread* thread2;
};
AppWorldLogic.cpp
#include "AppWorldLogic.h"
#include <UnigineAsyncQueue.h>
using namespace Unigine;
using namespace Math;
class MeshProducerThread : public Thread
{
public:
MeshProducerThread()
{
callback = AsyncQueue::addCallback(AsyncQueue::CALLBACK_MESH_LOADED, MakeCallback(this, &MeshProducerThread::mesh_loaded));
}
~MeshProducerThread()
{
AsyncQueue::removeCallback(AsyncQueue::CALLBACK_MESH_LOADED, callback);
}
public:
void process()
{
while (isRunning())
{
mesh_id = AsyncQueue::loadMesh("core\\meshes\\material_ball.mesh");
wait();
Log::message("Thread %d: mesh loaded\n", getID());
AsyncQueue::takeMesh(mesh_id);
}
}
private:
void mesh_loaded(const char* name, int id)
{
if (mesh_id == id)
signal();
}
private:
int mesh_id = 0;
void* callback;
};
int AppWorldLogic::init()
{
thread1 = new MeshProducerThread();
thread1->run();
thread2 = new MeshProducerThread();
thread2->run();
return 1;
}
int AppWorldLogic::shutdown()
{
thread1->stop();
thread2->stop();
return 1;
}
避免死锁#
存在相互锁定的可能性,也称为 deadlock ;如果锁定对象的函数执行回调(callback)函数,该回调函数反过来调用同一锁定对象的函数,则可能发生这种情况。
景观地形的操作#
ObjectLandscapeTerrain类包含一组旨在获取风景数据和相交检测的线程安全方法。
AppWorldLogic.h
#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
#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;
}
与全球地形相交#
ObjectTerrainGlobal包含一组用于某些特殊情况的线程安全方法。
AppWorldLogic.h
#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
#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相关类直接涉及到主循环的线程中。他们没有提供同步机制。
为了从用户线程安全地对这些对象进行操作,应首先暂停主循环以避免干扰。然后,您可以运行所需数量的 jobs (题)来处理节点。完成所有计算题后,继续主循环。
通过处理与主循环相关的对象的所有Engine线程与Engine::swap()的同步来确保线程安全,其中将延迟删除对象。但是用户线程可以与主线程中的Engine::swap()并行执行,在这种情况下,您在Engine::swap()期间不应对与主循环相关的对象(例如Node)执行任何操作。
在某些典型情况下,建议使用以下对象:
- AsyncQueue 类对于异步加载资源很有用。
- Async 类用于以异步方式运行自定义任务。
独立于主循环的对象#
主循环中也没有涉及API成员,它们也没有同步算法。
您可以在任何线程中完全管理此类对象,但是请注意,如果需要将其发送到另一个线程(主循环或用户线程),则必须提供手动同步以确保其数据一致性。
为此,您可以自由决定使用include/UnigineThread.h文件中包含的任何方法和类或其他机制。
以下API成员被认为独立于主循环线程:
有关使用基于简单互斥锁(Mutex)的ScopedLock的手动同步的C ++实现的信息,请参见Thread C++ Sample。
与GPU相关的对象#
一些成员方法与Graphics API交互,后者仅在主循环中可用。一旦需要调用与gpu相关的函数,就必须将对象传递到主循环并在其中执行调用。
与渲染相关的 类(例如MeshDynamic)应被视为与gpu相关。
此外, Object 相关类具有与渲染相关的方法,例如render()和其他的。
下面,您将找到dynamic_03示例的源代码,该示例演示了如何使用异步执行的 Marching cubes 算法创建动态网格。
dynamic_03.usc
#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示例,该示例关于异步管理多个 Mesh Cluster。您可以将其复制并粘贴到项目的世界脚本文件中。
cluster_03.usc
#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;
}