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.保证在主循环中安全地使用Unigine API对象。 当涉及到多个用户线程时,事情变得更加复杂。
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.由于并非所有的API类都是线程安全的,因此应考虑API成员的行为类型,以在应用程序中实现安全的多线程处理。
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.仅允许一个线程同时访问数据,而为其他线程锁定数据,这就是为什么多个线程可能不得不互相等待直到其任务完成。
The following API members are considered to be thread-safe:以下API成员被认为是线程安全的:
Loading Nodes Asynchronously异步加载节点#
It is not allowed to load nodes in user threads. For that purpose the AsyncQueue Class is recommended to be used.不允许在用户线程中加载节点。为此,建议使用 AsyncQueue类。
AppWorldLogic.h 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 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;
}
Avoiding Deadlocks避免死锁#
There is a possibility 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.存在相互锁定的可能性,也称为 deadlock ;如果锁定对象的函数执行回调(callback)函数,该回调函数反过来调用同一锁定对象的函数,则可能发生这种情况。
Operations with Landscape Terrain景观地形的操作#
ObjectLandscapeTerrain classes contain a set of thread-safe methods intended for fetching landscape data and intersection detection. ObjectLandscapeTerrain类包含一组旨在获取风景数据和相交检测的线程安全方法。
AppWorldLogic.h 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 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;
}
Intersections with Global Terrain与全球地形相交#
ObjectTerrainGlobal contains a set of thread-safe methods intended for some special use cases. ObjectTerrainGlobal包含一组用于某些特殊情况的线程安全方法。
AppWorldLogic.h 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 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;
}
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. Node类和Node相关类直接涉及到主循环的线程中。他们没有提供同步机制。
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.为了从用户线程安全地对这些对象进行操作,应首先暂停主循环以避免干扰。然后,您可以运行所需数量的 jobs (题)来处理节点。完成所有计算题后,继续主循环。
Thread safety is ensured by synchronization of all Engine's threads dealing with main-loop-dependent objects with Engine::swap(), where delayed deletion of objects is performed. But user threads can be executed in parallel with Engine::swap() in the main thread, in such cases you shouldn't perform any manipulations with main-loop dependent objects (such as nodes) during Engine::swap().通过处理与主循环相关的对象的所有Engine线程与Engine::swap()的同步来确保线程安全,其中将延迟删除对象。但是用户线程可以与主线程中的Engine::swap()并行执行,在这种情况下,您在Engine::swap()期间不应对与主循环相关的对象(例如Node)执行任何操作。
For some typical cases it is recommended to use the following objects:在某些典型情况下,建议使用以下对象:
- AsyncQueue Class is useful for asynchronous loading of resources.AsyncQueue 类对于异步加载资源很有用。
- Async Class is intended for running custom tasks in asynchronous way.Async 类用于以异步方式运行自定义任务。
Main-Loop-Independent Objects独立于主循环的对象#
There are also API members which are not involved into the main loop, they don't have synchronization algorithms as well.主循环中也没有涉及API成员,它们也没有同步算法。
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 any methods and classes contained in the include/UnigineThread.h file or other mechanisms at your discretion.为此,您可以自由决定使用include/UnigineThread.h文件中包含的任何方法和类或其他机制。
The following API members are considered to be independent of the main loop threads:以下API成员被认为独立于主循环线程:
See the Thread C++ Sample for C++ implementation of manual synchronization using the ScopedLock based on the simple mutex (Mutex).有关使用基于简单互斥锁(Mutex)的ScopedLock的手动同步的C ++实现的信息,请参见Thread C++ Sample。
GPU-Related Objects与GPU相关的对象#
Some member 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. 一些成员方法与Graphics API交互,后者仅在主循环中可用。一旦需要调用与gpu相关的函数,就必须将对象传递到主循环并在其中执行调用。
The Rendering-Related Classes (e.g. MeshDynamic) should be considered as gpu-related. 与渲染相关的 类(例如MeshDynamic)应被视为与gpu相关。
Also, the Object-Related Classes have rendering-related methods, such as render() and other ones.此外, Object 相关类具有与渲染相关的方法,例如render()和其他的。
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示例的源代码,该示例演示了如何使用异步执行的 Marching cubes 算法创建动态网格。
dynamic_03.usc 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;
}
Threads in UnigineScriptUnigineScript中的线程#
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 modified asynchronously and then swapped with the original object on the flush step.使用UnigineScript工作流程时,还应记住,不得直接在主循环之外修改与主循环相关的对象。相反,建议为此类对象创建一个副本,该副本将被异步修改,然后在flush步骤上与原始对象交换。
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.在下面,您将找到一个UnigineScript示例,该示例关于异步管理多个 Mesh Cluster。您可以将其复制并粘贴到项目的世界脚本文件中。
cluster_03.usc 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;
}