Cdorsu Posted June 10, 2017 Share Posted June 10, 2017 (edited) 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); 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 June 10, 2017 by Cdorsu 1 Link to comment Share on other sites More sharing options...
MAMRETRAS Posted June 10, 2017 Share Posted June 10, 2017 Why using std::thread m_Thread; instead of using namespace std; ... thread m_Thread; P.S. Frumos tutorial, mai greut pentru cei incepatori. Link to comment Share on other sites More sharing options...
Cdorsu Posted June 10, 2017 Author Share Posted June 10, 2017 (edited) 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! Edited June 10, 2017 by Cdorsu Link to comment Share on other sites More sharing options...
NUSUNTRENKO Posted June 11, 2017 Share Posted June 11, 2017 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? 1 Link to comment Share on other sites More sharing options...
MAMRETRAS Posted June 11, 2017 Share Posted June 11, 2017 1 hour ago, NUSUNTRENKO said: PS2: doar eu trebuie sa dau scroll in dreapta sa citesc unele randuri/comentarii? Nope, me too. It's because the text isn't wrapped. Link to comment Share on other sites More sharing options...
Cdorsu Posted June 12, 2017 Author Share Posted June 12, 2017 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 More sharing options...
SwiftBrotherHD Posted June 25, 2017 Share Posted June 25, 2017 Topic closed. Link to comment Share on other sites More sharing options...
Recommended Posts