Jump to content
Hostul a fost schimbat. Daca vedeti serverul offline readaugati rpg.b-zone.ro sau 141.95.124.78:7777 in clientul de sa-mp ×

[C++] Cum sa implementam un Worker Thread


Cdorsu
 Share

Recommended Posts

Jocurile sunt aplicatii care au nevoie sa faca foarte multe lucruri in timp real. Pentru a face mai multe lucruri e nevoie sa folosim multithreading. Dar creearea unui thread pentru fiecare lucru pe care trebuie sa-l facem intr-un cadru este destul de costisitoare (si chiar inutila). De aceea, eu m-am gandit sa creez un WorkerThread care trebuie sa fie creeat o singura data (atunci cand aplicatia este pornita) si poate sa execute mai multe functii (care pot fi schimbate atunci cand actualizam un cadru pe care trebuie sa-l desenam).

Multithreadingul este foarte folositor atunci cand avem operatii pe care le putem face de-a lungul a mai multe cadre (exemplu: sa calculam o ruta - nu e neaparat sa facem asta intr-un singur cadru)

Pentru a putea folosit la maxim aceasta clasa, ar trebui sa creeam o alta clasa ThreadManager care va creea la inceput atatea thread-uri cate are procesorul si le va aloca atunci cand va fi nevoie de ele. De asemenea, aceste threaduri pot fi folosite si la initializare (un thread incarca cateva animatii, altul se ocupa de compilarea si creearea shaderelor), threadul principal doar dandu-le "activitati" threadurilor, iar atunci cand ramane ajunge la sfarsitul initializarii, va apela o functie care va astepta toate threadurile. (Aici este destructorul)

Exemplu: Procesorul are 4 nuclee si 8 threaduri. Vom creea 8 threaduri pe care le vom aloca in timpul jocului.

!Atentie: Trebuie sa folositi un compilator C++11.

 

#include <functional> // std::function
#include <thread> // std::thread
#include <condition_variable> // std::condition_variable
#include <mutex> // std::mutex, std::lock_guard

class CWorkerThread // Deci, aceasta este clasa "principala", worker threadul va fi implementat aici
{
	std::thread m_Thread; // acesta este threadul care va lucra
	std::condition_variable m_ConditionVariable; // cu aceasta variabila vom putea "admormi" si "trezi" threadul
	std::mutex m_Mutex; // acesta este mutex-ul care va bloca / debloca clasa (doar functia Run() )
	std::function<void( )> m_Function; // aceasta este functia care va fi executata
	bool m_bHasFunction = false; // Aceasta variabila marcheaza faptul ca avem o functie de executata; Initial va fi false insemnand ca nu executam nicio functie
	bool m_bFinish = false; // Aceasta variabila va cere "inchiderea" (unirea thread-ului cu threadul principal) threadului; Initial va fi false insemnand ca nu e nevoie sa inchidem threadul
	bool m_bDone = true; // Aceasta variabila marcheaza faptul ca am terminat de lucrat / inca lucram; Initial va fi setata ca true insemnand ca deja am terminat ultimul lucru pe care l-am avut de facut (nimic)
public:

	CWorkerThread( ) // Aici doar vom creea thread-ul
	{
		m_Thread = std::thread( &CWorkerThread::Run, this ); // Totul se va petrece in functia Run()
      // constructorul std::thread() va creea un thread care va rula functia Run dintr-un obiect de tip CWorkerThread (&CWorkerTHread)
      // dupa ce am spus ce functie va executa threadul, vom pune argumentele separate prin virgula (aici doar this) - Nu mai e nevoie de paranteze
      // Alte exemple: std::thread([](int p1, int p2) { std::cout << p1 << ' ' << p2; }, /*aici trebuie sa punem parametrii*/3,5);
      // In programarea orientata pe obiecte, daca o metoda este trecuta ca fiind fara parametri, aceasta are un parametru (acel this), acesta este motivul pentru care avem acel 'this' acolo
      // Exemplu: CWorkerThread() pare sa nu aiba niciun argument, dar el, de fapt, are argumentul this
	}

	~CWorkerThread( )
	{
		Stop( ); // Cerem inchiderea threadului
		m_Thread.join( ); // Unim thredul cu threadul principal
	}

public:
	inline bool hasFunction( ) // returnam valoarea variabile m_bHasFunction; Am spus mai sus ce reprezinta
	{
		return m_bHasFunction;
	}
	inline bool getEndState( ) // returnam valoarea variabile m_bFinish; Am spus mai sus ce reprezinta
	{
		return m_bFinish;
	}
	inline bool isDone( ) // returnam valoarea variabile m_bDone; Am spus mai sus ce reprezinta
	{
		return m_bDone;
	}
	inline void SetFunction( std::function<void( )> && function ) // Cu ajutorul acestei functii, vom seta functia pe care o va executa threadul
	{
      // Am folosit &&, deoarece vreau ca acel obiect (function) sa fie mutat aici (operatie mult mai rapida decat copierea clasica (fara &&); De asemenea, puteam folosi & care reprezinta referinta la obiectul respectiv) 
		m_Function = std::move( function ); // Din moment ce am primit un obiect care trebuie mutat undeva ca argument, il vom muta in variabila m_Function
		m_bHasFunction = true; // Tocmai ce am primit o functie pe care trebuie sa o executam
	}
	inline void Start( )
	{
		m_ConditionVariable.notify_all( ); // Spunem tuturor threadurilor "adormite" sa revina la viata; (doar un thread (m_Thread))
      //Observatie: Atunci cand am trimis un thread "la somn", i-am precizat si un predicat (acea functie lambda). Atunci cand noi ii spunem sa revina la viata, threadul va evalua predicatul si va reveni la viata doar daca predicatul este adevarat
	}
	inline void Stop( )
	{
		m_bFinish = true; // vrem ca threadul sa fie oprit
		m_ConditionVariable.notify_all( ); // spunem tuturor threadurilor sa revina la viata; (doar un thread (m_Thread))
	}
private:
	void Run( ) // Aceasta este functia de baza a clasei
	{ 
		std::unique_lock<decltype( m_Mutex )> locker( m_Mutex ); // Incepe cu declararea unui obiect de tip std::unique_lock<std::mutex>;
      // Declararea acestui obiect va bloca mutex-ul, deci si functia;
      // va bloca = niciun alt thread nu va mai putea executa aceasta functie (daca va dori, va trebui sa astepte pana cand este deblocata)
      // Aceasta functie va fi apelata doar de un singur thread (m_Thread), dar este obligatoriu sa avem un std::unique_lock pentru a putea folosi m_ConditionVariable
		while ( true ) // Incepem un loop infinit
		{
			m_bDone = true; // Am terminat de executat ultima functie primita
			m_bHasFunction = false; // nu mai avem nicio functie de executat
#if _DEBUG || DEBUG
			OutputDebugStringW( L"Finished task, waiting for another one\n" ); // Afisam un mesaj corespunzator daca suntem in Debug Mode
#endif
			m_ConditionVariable.wait( locker, [ this ] { return this->hasFunction( ) || this->getEndState( ); } ); // "Adormim" threadul;
          // Threadul va putea reveni la viata DOAR atunci cand are o functie SAU e nevoie sa fie inchis (getEndState())
          // El va incerca sa revina la viata atunci cand va fi notificat (notify_all() / notify_one()) de aceeasi variabila m_ConditionVariable care l-a adormit
			if ( getEndState( ) == true )// Daca am ajuns aici, inseamna ca threadul a fost notificat sa se trezeasca. Primul lucru pe care il face, este sa verifice daca trebuie sa fie inchis
				break; // daca trebuie sa fie inchis, se va terminat (aici, acel break; este echivalent cu return; pentru ca dupa ce se termina loop-ul nu mai este nimic de executat in functie)
			// Daca am ajuns pana aici, inseamna ca am primit o functie pe care trebuie sa o executam;
			m_bDone = false; // asa ca marcam ca inca nu am terminat de executat functia
			m_Function( ); // si incepem sa executam functia
		}
	}
};

 

Acum sa va arat un exemplu (cum l-am folosit eu);

300px-Fractal_tree.svg.png

Acesta este un copac fractal (fractal tree).

El este facut foarte simplu: la capatul trunchiului creeam doua crengute(una in dreapta, una in stanga). De aici, la fiecare capat al unei crengi vom creea doua crengute (una in dreapta una in stanga) intr-un proces recursiv. Nu o sa va arat cum am facut asta. Este o provocare (destul de usoara, cred eu) pentru voi.

In fine, trecem peste asta. Am initializat la inceputul programului un buffer in care retin toate punctele copacului

 


bool CApplication::Initialize()
{
	m_fAngle = 3.14f / 4.f; // ~ 90 de grade (~ - pentru ca PI are mult mai multe zecimale)
	m_FractalTree = new CFractalTree( );
	if ( !m_FractalTree->Initialize( 10, 0.1f, m_fAngle ) ) // 10 - lungimea initiala; 0.1 - lungimea minima; m_fAngle - unghiul dintre 2 crengi (asa am vrut eu :D) 
		return false;
	m_WorkerThread = new CWorkerThread( ); // Creeam un worker thread care se va ocupa de actualizarea punctelor copacului
}

void CApplication::Run()
{
	if (SageataJos)
	{
		m_fAngle += 0.01f;	
	}
	if (SageataSus)
	{
		m_fAngle -= 0.01f;
	}
	std::function<void( )> func = std::bind(
		&CFractalTree::ActualizeazaPuncte, m_FractalTree,
		m_fAngle ); // Cum spuneam mai sus, vom crea un apel catre o functia ActualizeazaPuncte din clasa CFractalTree, urmand argumentele separate prin virgula (fara ()), primul argument fiind obiectul din care vom apela functia (this) si al doilea argument fiind unghiul cu care vom modifica punctele
	m_WorkerThread->SetFunction( std::move( func ) );
	m_WorkerThread->Start( );
  	if ( m_WorkerThread->isDone( ) )
	{
		// Actualizeaza punctele si in bufferul pe care il desenam
	}
}
void CApplication::Shutdown() // Sa nu uitam sa punem la loc jucariile dupa ce ne-am jucat cu ele
{
  	if ( m_FractalTree )
	{
		m_FractalTree->Shutdown( );
		delete m_FractalTree;
		m_FractalTree = 0;
	}
	if ( m_WorkerThread )
	{
		delete m_WorkerThread;
		m_WorkerThread = 0;
	}
}

Controls: Mouse - Controlam camera

V - schimbam camera (First Person / Third Person) // Nota: Cu First Person nu prea o sa se vada nimic

O - activam / dezactivam cerul

Sageata Sus / Sageata Jos - modificam unghiul dintre 2 crengute

Download link: https://www.mediafire.com/?awuoa0qowdpbjd2

DirectX 11+;

Tot exista un mic "hit" pe framerate, dar este mult mai mic. Eu reusesc sa mentin un decent 200-250 de FPS-uri.

Sper ca ati inteles cat de cat ce am vrut eu sa fac aici. Daca aveti intrebari, nu ezitati sa le lasati intr-un reply la acest topic.

Edited by Cdorsu
Link to comment
Share on other sites

2 hours ago, eB Teodor said:

Why using


std::thread m_Thread;

instead of


using namespace std;
...
thread m_Thread;

P.S. Frumos tutorial, mai greut pentru cei incepatori.

 

Atunci cand lucrezi cu mai multe librarii e posibil sa fie implementari ale unor clase cu acelasi nume. De obicei, e bine sa fii cat mai explicit.

PS: Multumesc! :D 

Edited by Cdorsu
Link to comment
Share on other sites

17 hours ago, NUSUNTRENKO said:

Noice, acum tre' sa urmezi cu thread safety.

PS1: evita new/delete, use smart pointers.

PS2: doar eu trebuie sa dau scroll in dreapta sa citesc unele randuri/comentarii?

In momentul in care am facut aceasta implementare, nu prea m-am gandit ca vor fi mai multe threaduri care vor accesa aceleasi date.

Eu cred ca thread safety ar trebui sa se faca extern (cel care scrie threadurile sa aiba grija ca aceste threaduri nu vor accesa aceleasi date, rezultand Data Race).

 

M-am cam obisnuit sa folosesc pointerii "vechi' :))

Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
 Share

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.