Jump to content

Mutex Multithreading in LE


Canardia
 Share

Recommended Posts

Here is a demo which demonstrates multithreading in LE2.

It uses 3 threads plus the main thread:

1) Thread 1 loads models from memory

2) Thread 2 turns them

3) Thread 3 waits until all models are loaded and then gravitates them

 

One model must be loaded once in the main thread, but that is not a big issue as you can reuse them anyway and loading 1 models doesn't take very long, so it's only a small delay when the game starts. After that everything is multithreaded and there is no waiting time while loading new models and levels.

 

Here is the source code:

#include "engine.h"
#include "pthread.h"
#include <vector>
#include <string>
#define NUMTHREADS 3
#define NUMMODELS 1000
#define LOCK pthread_mutex_lock(&count_mutex);
#define UNLOCK pthread_mutex_unlock(&count_mutex);
#define WriteText(y,s) SetColor(Vec4(0,0,0,1));DrawText(2,y+1,s.c_str());SetColor(Vec4(1));DrawText(1,y,s.c_str());
using namespace std;
pthread_t callThd[NUMTHREADS];
pthread_mutex_t count_mutex;
vector<TMesh> model;
string s1="Thread 1 Idle",s2="Thread 2 Idle",s3="Thread 3 Idle";

void *LoadModels(void *arg)
{
LOCK s1="Thread 1 Loading Oildrums"; UNLOCK
for(int i=0;i<NUMMODELS;i++)
{
	LOCK
	BP e=LoadModel("oildrum.gmf");
	MoveEntity(e,Vec3(-9+2*(i/10%10),2+1*(i/100),2*(i%10)));
	TurnEntity(e,Vec3(90,0,0));
	SetBodyMass(e,1);
	EntityType(e,1);
	SetBodyElasticity(e,2);
	SetBodyGravityMode(e,0);
	model.push_back(e);
	UNLOCK
	Sleep(20);
}
LOCK s1="Thread 1 Done"; UNLOCK
pthread_exit(NULL);
}

void *TurnModels(void *arg)
{
LOCK s2="Thread 2 Turning Oildrums"; UNLOCK
bool done=false;
while(!done)
{
	LOCK int n=model.size(); UNLOCK
	for(int i=0;i<n;i++)
	{
		LOCK
		TurnEntity(model.at(i),Vec3(0.1*AppSpeed(),AppSpeed(),0.0*AppSpeed()));
		UNLOCK
	}
	Sleep(1);
	if(n>=NUMMODELS)done=true;
}
LOCK s2="Thread 2 Done"; UNLOCK
pthread_exit(NULL);
}

void *GravitateModels(void *arg)
{
LOCK s3="Thread 3 Waiting for all Oildrums to arrive"; UNLOCK
bool done=false;
while(!done)
{
	LOCK int n=model.size(); UNLOCK
	if(n==NUMMODELS)
	{
		LOCK s3="Thread 3 Waiting 5 seconds"; UNLOCK
		Sleep(5000);
		LOCK s3="Thread 3 Gravitating Oildrums"; UNLOCK
		for(int i=0;i<n;i++)
		{
			LOCK
			SetBodyGravityMode(model.at(i),1);
			UNLOCK
		}
		done=true;
	}
	Sleep(1);
}
LOCK s3="Thread 3 Done"; UNLOCK
pthread_exit(NULL);
}

int main(int argc, char* argv[])
{
Initialize();
Graphics(800,600);

pthread_attr_t attr;
pthread_mutex_init(&count_mutex,NULL);
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE);

TFramework fw=CreateFramework();
HideEntity(LoadModel("oildrum.gmf"));		// initialize FBO for other threads
TCamera cam=GetLayerCamera(GetFrameworkLayer(0));
MoveEntity(cam,Vec3(0,5,-10));
TurnEntity(CreateDirectionalLight(),Vec3(45,45,0));
EntityType(CreateTerrain(128),1);
SetFarDOF(1);
SetDistanceFog(1);
SetBackgroundColor(Vec4(0.8,0.8,1,1));
Collisions();

// create other threads
pthread_create(&callThd[0],&attr,LoadModels,(void*)1);
pthread_create(&callThd[1],&attr,TurnModels,(void*)2);
pthread_create(&callThd[2],&attr,GravitateModels,(void*)3);

while( !AppTerminate() && !KeyHit() )
{
	LOCK
	UpdateFramework();
	RenderFramework();
	SetBlend(BLEND_ALPHA);
	WriteText(20,s1);
	WriteText(40,s2);
	WriteText(60,s3);
	SetBlend(BLEND_NONE);
	UNLOCK
	Flip(0);
	Sleep(1);
}

// kill other threads
pthread_detach(callThd[0]);
pthread_detach(callThd[1]);
pthread_detach(callThd[2]);

// wait for other threads to finish
pthread_join(callThd[0],NULL);
pthread_join(callThd[1],NULL);
pthread_join(callThd[2],NULL);

// destroy mutex engine
pthread_attr_destroy(&attr);
pthread_mutex_destroy(&count_mutex);
pthread_exit(NULL);
return Terminate();
}

post-2-0-80875300-1311248007_thumb.png

And here is the complete Code::Blocks project with executable demo also:

 

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

One model must be loaded once in the main thread, but that is not a big issue as you can reuse them anyway and loading 1 models doesn't take very long, so it's only a small delay when the game starts. After that everything is multithreaded and there is no waiting time while loading new models and levels.

 

So you still have to load unique models in your main thread which still cause pauses to your game?

Link to comment
Share on other sites

So you still have to load unique models in your main thread which still cause pauses to your game?

No, it doesn't pause your game, because it happens only before the game starts, and you can then even do some intro/menu/video in another thread, while the main thread is busy.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

No, it doesn't pause your game, because it happens only before the game starts, and you can then even do some intro/menu/video in another thread, while the main thread is busy.

 

It only happens at the start if that's how you program your game. Loading hundreds or thousands of unique models at the start of an RPG or MMO isn't practical. Loading them on the fly is preferred to get a streaming world. Weren't you big on streaming worlds without precaching everything at the start? I think that's one of the biggest arguments for multithreading in LE.

 

You would want your main thread to do the manipulation of your models and your secondary thread to be the one that loads them to give you the power of streaming your world.

Link to comment
Share on other sites

Well do it the other way around: put your game in thread 1, and leave the main thread for loading models then. It's the same effect, except then there is no waiting at all.

 

I'm gonna make a new demo next, which uses that idea :)

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

Is that really possible? If you try to move a model that isn't fully loaded wouldn't it crash? If you put mutex around wouldn't that pause leaving you with the same issue? I would think you might have to spam your code checking if each model is NULL or not maybe?

Link to comment
Share on other sites

You can't move models which are not loaded, because they are not in the model vector yet. So everything which is in the model vector is already loaded.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

I can put the main thread which loads the engine and creates the graphics window to another thread, but the problem remains that the first LoadModel must happen from that thread.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

Well, it's basically good for using the other cores too, instead of just one. Without mutex you can't call any LE2 commands on the other threads. So for example if you have some heavy AI calculations, you can now do them on another thread and still use LE2 commands there.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

How expensive is all the locking though? For any process that does a lot without using LE commands I could see this as being advantageous but otherwise will it end up being any more efficient at all?

 

Interesting work by you and Lazlo though!

Intel Core i5 2.66 GHz, Asus P7P55D, 8Gb DDR3 RAM, GTX460 1Gb DDR5, Windows 7 (x64), LE Editor, GMax, 3DWS, UU3D Pro, Texture Maker Pro, Shader Map Pro. Development language: C/C++

Link to comment
Share on other sites

It's up to you to make the locking so that you don't let time critical parts get sliced by other threads.

Normally the locking is just concurrent behaviour, and the CPU does things all the time.

When one thing is locked, then it does another, and so an, so there is no time lost in that sense.

 

I don't think the lock/unlock command itself takes much time, of course this can be tested also.

And ultimately, you should get number of cores times faster programs, if you use the multithreading in the right way.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

The locking rule is pretty simple. You need to lock always:

1) All LE2 commands, except Flip

2) All variables which are used in other threads too, even if only for read access

 

Don't lock too much, but lock only what is really needed, else you get chunky multithreading and not fluid multithreading.

 

In addition to locking, you need to add at least a Sleep(1) to each thread also, because else the other threads will run very slow and at inconsistent speed, because they are waiting for CPU idle time.

Ryzen 9 RX 6800M ■ 16GB XF8 Windows 11 ■
Ultra ■ LE 2.53DWS 5.6  Reaper ■ C/C++ C# ■ Fortran 2008 ■ Story ■
■ Homepage: https://canardia.com ■

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...