Toon posts:

[C++] communicatie tussen threads + generieke data

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
sorry, ik kon geen betert topictitel verzinnen :P (suggesties welkom)

ik ben mn game engine aan het ombouwen voor multithreading support, maar loop tegen een designprobleem aan. mn huidige opzet 'werkt', maar de dataoverdracht tussen threads behoeft me wat al teveel code. bovendien gok ik dat er mooiere oplossingen zijn (maar ik kan ze niet vinden).
ook zou ik het datagebeuren iets mooier willen.

dit is in semi-pseudocode de huidige opzet:

<toelichting>

eerst even een uitleg om het lezen wat makkelijker te maken..
een Thread heeft een MessageQueue die hij in de 'main thread loop', operator()(), polled op nieuwe berichten.

een voorbeeld: stel er is een nieuw bericht (in MessageData.message) voor de graphics thread: 'e_GraphicsThreadMessage_CreateContext'..
de CreateContext functie in die thread leest message->messagedata->parameters uit voor bijv de resolutie en hoeveel BPP.
hij doet zn ding en zet in message->messagedata->returnvalues->values een boolean of het al dan niet is gelukt.
vervolgens roept hij message->messagedata->returnvalues->ready() aan.
daardoor wordt in returnvalues de isReady bool op true gezet.

de aanroepende thread is al die tijd diezelfde isReady aan het pollen (via WaitForReady());
die leest uit message->messagedata->returnvalues->values de boolean uit en weet zo of de CreateContext is geslaagd.

</toelichting>


C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// data gedeelte

enum e_DataType {
    e_DataType_Integer = 1,
    e_DataType_Boolean = 2,
    e_DataType_Float = 3,
    ... etcetera ...
};
  
class Data {
    Data(e_DataType dataType) : dataType(dataType);
    e_DataType dataType;
};

class BooleanData : public Data {
    BooleanData(bool value) : Data(e_DataType_Boolean) { data = value; }
    bool data;
};

class IntegerData : public Data {
    IntegerData(int value) : Data(e_DataType_Integer) { data = value; }
    int data;
};

.. etcetera ..


// functionele gedeelte

class DataArray {
    std::list<Data*> elements;
};

class ReturnValues {
    void WaitForReturn(); // called from sender: poll if receiver is ready
    void Ready(); // called from receiver when message has been processed
    bool isReady;
    DataArray data;
};

struct FunctionData {
    DataArray parameters;
    ReturnValues returnValues;
};

struct MessageData {
    MessageType message;
    FunctionData *functionData;
};

class MessageQueue {
    std::list<MessageData> queue;
};

class Thread {
    virtual operator()() = 0;
    MessageQueue messageQueue;
};


hier een diagram van dit gebeuren, dat maakt het wellicht wat duidelijker..
(let niet op mn UML syntax, die is ongetwijfeld onjuist :P maar t gaat om het idee)

Afbeeldingslocatie: http://www.knageroe.nl/media/gfx/blunted_threadcomm.jpg

dan nu het probleem: zo zien de uitvoerende functie en de ontvangende functie er uit: (= veel te veel code)

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// de functie in de thread die de 'CreateContext' opdracht geeft aan de graphics thread:

FunctionData *fdata = new FunctionData();
IntegerData *Iwidth = new IntegerData(width);
IntegerData *Iheight = new IntegerData(height);
IntegerData *Ibpp = new IntegerData(bpp);
fdata->parameters.PushElement(Iwidth);
fdata->parameters.PushElement(Iheight);
fdata->parameters.PushElement(Ibpp);

renderer3DTask->messageQueue.PushMessage(e_Renderer3DThreadMessage_CreateContext, fdata);
fdata->returnValues.WaitForReturn();
    
BooleanData *success = static_cast<BooleanData*>(fdata->returnValues.data.PopElement());

if (!success->data.data) {
    Log(e_FatalError, "GraphicsSystem", "Initialize", "Could not create context");
} else {
    Log(e_Notice, "GraphicsSystem", "Initialize", "Created context, resolution " + int_to_str(width) + " * " + int_to_str(height) + " @ " + int_to_str(bpp) + " bpp");
}

delete success;
delete Iwidth, Iheight, Ibpp;
delete fdata;


C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// de graphicsthread die de message gaat verwerken..

FunctionData *fdata = messageQueue.WaitForMessage(message);
switch (message) {
          
    case e_Renderer3DThreadMessage_CreateContext: {
        // parameters:
        //   IntegerData width
        //   IntegerData height
        //   IntegerData bpp
        //
        // return values:
        //   BooleanData success

        assert(fdata);

        int width = static_cast<IntegerData*>(fdata->parameters.PopElement())->data.data;
        int height = static_cast<IntegerData*>(fdata->parameters.PopElement())->data.data;
        int bpp = static_cast<IntegerData*>(fdata->parameters.PopElement())->data.data;
        bool success = CreateContext(width, height, bpp);

        BooleanData *Bsuccess = new BooleanData(success);
        fdata->returnValues.data.PushElement(Bsuccess);
    } break;

    if (fdata) fdata->returnValues.Ready();
}


probleem 1:
als ik nu voor elke functie die ik ga aanroepen in een andere thread zoveel moet typen word ik gestoord :P

probleem 2:
bovendien moeten de returnvalues ge'new'ed worden in de verwerkende thread, maar gedelete in de aanroepende thread. dat lijkt me niet netjes.. maarja, de returnvalue kan pas weg als de aanroepende thread em heeft gelezen, maar de verwerkende thread weet niet wanneer dat is, dus die kan em niet deleten. het moet wel een *pointer zijn, omdat ik em - naar mijn weten - anders niet kan static_cast'en naar het juiste type.

probleem 3:
de static_casts... op zich moet dat kunnen - ik moet me gewoon braaf houden aan de parameters/return values van de functie in de verwerkende thread. toch zou het fijn zijn als die casts ergens netjes werden verstopt zodat ik daar kan checken op het typeID en een mooie error kan geven als ik een fout type gebruik.

probleem 4:
ik moet voor elk type data een nieuwe class maken, ondanks dat er verder weinig gebeurt in die classes.

probleem 5:
ik heb uberhaupt een sterk gevoel dat dit een stuk beter kan. mijn sterke gevoelens zijn over het algemeen gegrond ;)

voorwaarde:
datatypen moeten een vast 'id' hebben zoals met mn enumlijstje. dit omdat ik de data uiteindelijk ook wil kunnen saven en weer loaden - dan moeten alle datatypes dus hetzelfde id hebben en niet ineens verschoven zijn.

In <dit> topic laat .oisyn een oplossing zien voor een ander probleem, maar ik heb het vermoeden dat iets soortgelijks (met templates oid) hier wellicht ook van toepassing is. alleen werkt dat met een dynamisch enum - mijn voorwaarde is dus dat elk datatype een vast id heeft, dus dat wordt - lijkt me - lastig.

ik hoop dat het allemaal een beetje duidelijk is, elke suggestie is welkom! vragen ter verduidelijking ook, tis voor mij natuurlijk allemaal duidelijk maar het begrijpen van code van anderen is natuurlijk een stuk lastiger..

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

C++:
1
2
3
4
5
6
7
8
9
10
class Message
{
  public:
  void Wait() { m_sem.wait(); }
  void Handle() { Execute(); m_sem.post(); }
  protected:
  virtual void Execute() = 0;
  private:
 Semaphore m_sem;
}


Thread:
C++:
1
2
3
4
5
void MessageQueue::HandleMessage()
{
  Message* msg = m_queue.get();
  msg->Handle(); 
}


Calling thread:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SomeMessage() : public Message
{
  public:
  SomeMessage(int dataX, bool dataY);
  protected:
  virtual void Execute();
  private:
  int m_dataX;
  bool m_dataY;
};

void SomeMessage::Execute()
{
  // doe wat je moet doen.
}

void DoSomething(int x, bool y)
{
  SomeMessage(x, y).Send();
}


Dit is maar wat basiscode. Er zijn veel varianten op dit principe met elk hun voor en nadelen.
Je kan bvb asynchrone (fire-and-forget) of synchrone messages hebben. Je kan de Send implementeren
op de message queue of op de message. Er zijn verschillende manieren om in je Execute() functie aan het object te raken waarop je moet werken. Je kan ook Send()/Post() semantiek gebruiken om echte futures (zie Herb Sutter's blogs) te implementeren. Er zijn veel verschillende mogelijkheden afhankelijk van je architectuur.
Kijk ook even naar de boost::shared_ptr. Als je shared_ptr's naar je message kan passen naar die andere thread dan los je al zeker het lifetime probleem van je object op. Bovendien koppelt dit het asynchroon/synchroon zijn van je message los van het messagetype: door een shared_ptr in je verzendende thread te houden of te vernietigen kan je je message respectievelijk synchroon/asynchroon maken, gegeven dat je in het synchrone geval ook wacht op de message.

Wat ik zeker niet zou doen is pollen. Dit kan een averechts effect hebben. Zowat elk OS werkt met timeslices. In het geval je gaat busyloopen op isReady duw je de CPU usage naar 100% terwijl je OS helemaal niet weet dat je eigenlijk op die andere thread staat te wachten. Daarom werk je best met synchronisatie primitieven om te blokkeren op de response.
Beter is zelfs nog om waar mogelijk een Post() te doen (asynchroon zenden), daarna nog wat processing te doen en pas dan gaan wachten op het resultaat. In een multi-core systeem kan dan de andere thread eventueel op de andere core de message verwerken. Dit doe je echter best niet met kleine hoeveelheden data aangezien je dan door cache-effecten de boel weer kan vertragen, tenzij je weer een geshared cache hebt, etc. etc.

Multi-core is helemaal niet simpel, bewijst deze link: http://www.ddj.com/hpc-high-performance-computing/212201163

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

Verwijderd

Even een korte reply met kreten die je zou kunnen toepassen om je in een richting te sturen:
probleem 1: Je zou hier templates kunnen gebruiken
probleem 2: je zou hier smart pointers (shared_ptr, zie bericht 2 ) kunnen gebruiken.
probleem 3: Dit is imho niet echt een probleem, als je je templates van p1 netjes doet, kun je je casts wel verbergen. Je kan ook rtti gebruiken ( niet mijn favoriet.. ), dit staat ook als voorwaarde beschreven.

Ik ga met H!ghguy mee, dat je beter asynchroon berichten kan sturen. Dan kun je in je aaroepende thread verder werken ( met het createcontext voorbeeld is het misschien handig om wel even te wachten.. )

succes
- dm

Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
edit: @dirkmay: moet even weg, zal straks reageren op je post, bedankt iig!
@highguy: dank je voor je uitgebreide reactie :)

mijn architectuur impliceert dat het om een (semi- (?))synchrone message gaat. ik zal even toelichten hoe het werkt in mijn systeem (de vraag is dan of mijn architectuur wel goed is)

ik heb systemen - bijv graphics, physics, sound .. kortom, losstaande 'units' die iets moeten doen met data.

elk systeem draait een thread, die de volgende opdrachten kent:
- init
- 'render frame'
- shutdown

de gfx system thread bijvoorbeeld krijgt 60x per seconde (in het ideale geval ;)) de opdracht van de scheduler om een frame te renderen. (andere systemen draaien wellicht met een ander fps)

de system threads mogen uitdrukkelijk geen cpu vreten - dwz, hun enige taak is het sturen en volgen van subtaken die ze naar de taskmanager sturen.

voorbeeld van wat de gfx system thread zou kunnen gaan doen als het van de scheduler de 'render frame' opdracht krijgt:

1. stuur naar taskmanager de opdracht "clear buffers"
wacht tot 1 klaar is

2.1 stuur naar taskmanager de opdracht "selecteer alle geometry zichtbaar vanuit main camera"
2.2 stuur naar taskmanager de opdracht "selecteer alle geometry zichtbaar vanuit rear view mirror camera" (om even een racegame als voorbeeld te nemen)
wacht tot 2.1 en 2.2 klaar zijn

3. stuur naar taskmanager de opdracht "render alle geselecteerde geometry"
wacht tot 3 klaar is

4. wacht op nieuwe message van scheduler

de taskmanager op haar beurt zet de opdrachten in haar queue, die vervolgens verwerkt wordt door worker threads die beschikbaar zijn. (dit zijn normaliter <numcore> threads, de enige threads die wel cpu mogen vreten).

dit impliceert dus dat de systeemthreads moeten weten als een opdracht verwerkt is - pas dan kunnen ze verder met volgende opdrachten. eigenlijk is zo'n systeemtaak (het renderen van een frame) dus een seriele taak met daarin parallele subtaken.

het voorbeeld dat je geeft met de Semaphore::wait() doet - als ik het goed begrijp - hetzelfde als mijn ReturnValues.WaitForReady(); - het wacht (polled) tot iemand de message verwerkt heeft (toch?). Sowieso vraagt het pollen geen CPU - ik heb er een korte pauze per polling-loop inzitten. Enige nadeel is dan wat tijdsverlies als een opdracht net klaar is als de korte pauze begint - pas na die wait ziet de pollende functie dat de opdracht klaar is. omdat ik nogal wat opdrachten verstuur per frame zou dit toch al gauw een milliseconde of 20 per frame verlies zijn (maw; meer dan een fps'je of 1000/20=50 zouden systemen dan al niet meer kunnen halen) en dus wil ik het wel graag anders, maar ik zou niet weten hoe.

iig, eigenlijk doe ik al hetzelfde als wat je zegt: "Daarom werk je best met synchronisatie primitieven om te blokkeren op de response" - toch? Of is er een wezenlijk verschil tussen hoe zo een primitief blokkeert tov. mijn poll-met-pauze-methode?

de workerthreads zouden de opdrachtgevende functie eigenlijk weer moeten aanslingeren ipv dat de opdrachtgevende taak de workerthread polled (daar komt het immers op neer) of hij weer verder kan - maar dan zou de opdrachtgevende functie niet daadwerkelijk in 1 functie kunnen (lijkt me) maar zou de workerthread na elke opdracht een nieuwe functie moeten opstarten in de gfx system thread. dat lijkt me wat irritant..

mijn methode - met de 'begeleidende' opdrachtgevende thread - impliceert ook dat je andere tip niet werkt: "Beter is zelfs nog om waar mogelijk een Post() te doen (asynchroon zenden), daarna nog wat processing te doen en pas dan gaan wachten op het resultaat." de begeleidende thread immers doet eigenlijk al zoiets - het stuurt meerdere opdrachten 'tegelijk' en wacht tot die klaar zijn. volgens mij doe ik op die manier al een soort asynchrone synchrone send :P (vergeef me als ik je niet goed begrijp - deze materie en terminologieen zijn nieuw voor me)

je shared-pointer tip is zeker een handige, dat scheelt weer een lastige delete (die kan ik namelijk bij elke opdracht vergeten, dat vraagt om foutjes en dus nare geheugenlekken).

mijn vragen naar aanleidingen van je reactie zijn dus, samengevat:
- wat is het verschil tussen mijn manier van pollen en het blokkeren op de response van een synchonisatie-primitief?
- als ik een echt mooi systeem wil - en dat wil ik - kan ik het hele pollen/blokkeren dan beter weggooien ten faveure van het vanuit de workerthread iets aanslingeren-on-ready?
- is mijn ReturnValue gebeuren al niet een soort Future? er zitten waarden in (ReturnValue::parameters) die pas ingevuld worden als de workerthread er mee klaar is.
- begrijp ik goed dat je (met de Message::Execute()) de daadwerkelijke code die de worker thread moet uitvoeren in de message stopt, ipv dat je de workerthread een bericht zou sturen met het adres van een uit te voeren functie? zoiets klinkt imho erg mooi, maar moet er eerst meer over denken om te kijken wat dat voor implicaties heeft voor mijn architectuur. bijv - ik zou dan voor elke functie een subtype van een Message moeten maken, toch? dan heb ik alsnog een hele lap code nodig per opdracht. maar correct me if ik het helemaal verkeerd begrijp ;)

zoals jullie begrijpen uit mijn reactie is het allemaal nog een grote probleemsoep voor mij, ik heb nog wat meer overzicht nodig in de mogelijkheden en welke daarvan toepasbaar zijn in mijn systeem - of dat ik wellicht mijn systeem nog moet aanpassen om bepaalde mogelijkheden te benutten.

nu eerst even een eindje fietsen door het mooie winterweer om het allemaal wat te laten bezinken ;)

Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
Verwijderd schreef op maandag 26 januari 2009 @ 09:03:
Even een korte reply met kreten die je zou kunnen toepassen om je in een richting te sturen:
probleem 1: Je zou hier templates kunnen gebruiken
probleem 2: je zou hier smart pointers (shared_ptr, zie bericht 2 ) kunnen gebruiken.
probleem 3: Dit is imho niet echt een probleem, als je je templates van p1 netjes doet, kun je je casts wel verbergen. Je kan ook rtti gebruiken ( niet mijn favoriet.. ), dit staat ook als voorwaarde beschreven.

Ik ga met H!ghguy mee, dat je beter asynchroon berichten kan sturen. Dan kun je in je aaroepende thread verder werken ( met het createcontext voorbeeld is het misschien handig om wel even te wachten.. )

succes
- dm
idd, templates crossed my mind, weet alleen niet precies hoe ik ze in dit geval kan gebruiken. ik denk dat ik daar maar gewoon wat mee moet gaan experimenteren nu, kan ik tenminste verder, tnx :)
ps. de ingebouwde rtti lijkt me ook niets (slecht portable), maar een eigen rtti implementatie kan ik mee leven ;)

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Synchronisatie-primitieven zoals een semafoor gebruiken het OS om te blokken.
Wanneer je een Wait() equivalent doet dan gaat je OS de huidige thread van de Runnable-queue naar de Blocked-queue halen. De thread komt dan door de scheduler niet meer in aanmerking om CPU-tijd te krijgen.
Wanneer je het Signal() equivalent doet dan gaat je OS 1 van de geblokkeerde threads van de Blocked-queue terug in de Runnable-queue stoppen, zodat deze terug kan draaien.

C++:
1
2
while (!messageHandled)
  msleep(1);

Geeft volgende problemen:
- Je compiler mag deze lus niet optimaliseren. Als messageHandled eenmaal in een register zit kan het zijn dat deze loop nooit meer returnt. Om dit tegen te gaan wordt de messageHandled meestal volatile gezet.
- Je hebt een maximale nauwkeurigheid, zoals je zelf aangeeft, van de tijd die je sleep()'t + de onnauwkeurigheid die sleep() met zich meebrengt.
- Je busyloopt en gebruikt CPU-tijd. Op een single-core machine zorgt deze constructie er dus voor dat de thread waar je de message naar zendt niet aan de beurt komt. Je staat dus te wachten op niets. Bovendien kan het zijn dat als de huidige thread een hogere prioriteit heeft als de message-handling thread, je voor een lange poos zoet bent aangezien de scheduler de busyloop thread langer aan bod zal laten.

Bovendien hebben synchronisatie primitieven nog voordelen:
- Op een RTOS kun je priority inheritance hebben (zeker niet in windows ;) )
- Na de Signal() kan je OS meteen beslissen om de voorheen geblokkeerde thread te laten draaien. Dit kan in jouw architectuur als gevolg hebben dat de thread niet meteen naar de threadpool terugkeert (wat bij genoeg worker threads geen issue zou mogen zijn) maar ook dat de belangrijke controle-thread sneller kan verder werken. Je kan overigens ook eerst je thread terug aan de threadpool toevoegen en pas daarna Signal() aanroepen.
- Je kan zelf thread-prioriteiten zetten. Je controle thread heeft hogere prioriteit en moet draaien telkens als hij kan. Door te blokkeren maak je maximaal gebruik van de features van je OS.

Nadelen van die primitieven zijn natuurlijk dat je naar kernel space moet. Dit is een context switch die je liefst zo weinig mogelijk maakt. Ik weet overigens niet of Windows al futexen kent, maar deze kunnen je toch dat tikkeltje meer performance geven door enkel naar de kernel te gaan als dit echt nodig is.

Jouw ReturnValue gebeuren heeft idd iets mee van futures.

Ik zal later meer antwoorden, ik meot nu terug aan het werk.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
dank je voor de heldere uitleg, in afwachting van meer antwoorden kan ik het busywaiten dan alvast fixen :)

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Vervolg:

Het zenden van messages en dan vervolgens wachten op antwoord is geen slecht iets. Het geeft een vorm van impliciete synchronisatie tussen threads. Je zendt bvb een message naar de sound/gfx/physics/AI thread en wacht dan tot wanneer elke message afgehandeld is. Daarna update je de world en zend je opnieuw messages naar elk subsysteem. Ik denk ook dat dit is wat je wou.

Wanneer je elke worker-thread een event laat afvuren op het einde, stel je dan even de vraag wat die event moet doen? Ik zou zeggen: Hij wacht op een update en zet dan het subsysteem waar hij van komt opnieuw aan tot processen. Dan kun je beter gaan voor 1 thread/subsysteem en die laten synchroniseren met een barrier (zie boost::barrier).

Je hebt gelijk dat in mijn voorbeeld de code binnen de Execute() functie van de message zit. Dit hoeft echter niet meer te zijn als een call naar het object zelf, waarin dan de parameters (message members) meegegeven worden en de return value gestored wordt voor de caller. Je message maakt eigenlijk deel uit van de interface van je class. Je kan bij het plaatsen van je code 2 richtingen uit (of er ergens tussen):
- Je stopt alle code in je messages. Je subsystem class dient dan eigenlijk als context waarin de messages uitgevoerd worden en als thread-owner.
- Je stopt alle code in je subsystem class en gebruikt de messages eigenlijk om je subsystem class een "active" class te maken: Je message dient enkel om de functie uit te voeren in de thread-context van het subsystem. Wil je hierin nog een stap verder gaan dan kan je publieke functies maken in je subsystem class die dan een message naar zichzelf zendt, waarin je dan uiteindelijk je implementatie-functie aanroept.

Ik stel me op dit moment wel vragen bij jouw architectuur. Zoals ik het zie doe je iets als:
Master Thread -> Subsystem thread -> ThreadPool en het is deze laatste die ook effectief de processing doet. Wees hier enorm voorzichtig mee: teveel threads kunnen je performance verslechteren. Bovendien moet je opletten met de libraries die je gebruikt. OpenGL (of op z'n minst publiek beschikbare wrapper implementatie?) bvb verwacht dat je alle calls vanuit 1 thread doet wegens gebruik van Thread-Local Storage vermoed ik.
Bovendien kun je allerhande cache-effecten krijgen of kan het dispatchen van een call naar een andere thread veel langer duren dan het zelf uitvoeren van de call. Ik kan me inbeelden dat de subsystem thread enorm veel gaat wachten, terwijl hij misschien zelf ook werk zou kunnen verrichten. Threadpools zijn een fantastisch hulpmiddel om een bulk aan werk te verzetten, maar zijn overkill om je normale taken op uit te voeren.
Let ook op locking issues. Wanneer je alle werk naar threadpools verhuist heb je mogelijk opnieuw locks nodig rond je data structuren. Daar gaat het voordeel van multicore...

Persoonlijk zou ik naar volgende architectuur streven: behoud elk niveau van threading zoals je het nu hebt, maar stop het gros van het werk van een subsysteem in de thread van dit subsysteem. Zorg ervoor dat je alles mooi designt zodat je later gemakkelijk kan refactoren. Duw enkel werk naar je threadpools als je echt een bulk verwerking moet doen en liefst met read-only (zonder lock dus) data. Voor de rest blijf je in je eigen thread. Wanneer dit functioneel is, ga je optimizen mbv profiling. Een goeie hulp kan hierin zijn dat je je threadpool at compile- of run-time kan uitzetten (zodat alle werk op de calling thread uitgevoerd wordt).
Ga dan vergelijken: met of zonder thread-pool, bepaalde items wel of niet naar de thread-pool. Welke threads staan wanneer niets te doen? Wat kan je nog opsplitsen? Wat moet je samenvoegen?

Voor het profilen kun je bvb timing-punten inlassen op kritische punten. Die timings kun je dan gaan balanceren tot je het meeste uit je code haalt. Let er natuurlijk ook op dat jouw X-core machine zich misschien helemaal anders gedraagt dan een X*2 of X/2 machine ;)

Maak niet de fout van alles alleen sneller te willen maken. Soms is trager beter, bvb:
Je hebt 2 threads die beiden de threadpool gebruiken. De ene thread handelt telkens zijn werk af in 5ms, de andere in 3ms. Dan is het misschien beter om ervoor te zorgen dat de snelste thread enkele items niet naar de threadpool zendt. Hiermee is er mogelijk meer cpu-tijd beschikbaar voor de trage thread en duurt de huidige thread ook iets langer.

Lees je misschien even in in het Parallels for .NET framework. De Microsoft site is op dit moment even niet beschikbaar, anders had ik je even een goed artikel gezocht dat ik een tijdje geleden gelezen heb.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
wederom bedankt voor de dit keer zonodig nog uitgebreidere reactie :) erg leerzaam!
Dan kun je beter gaan voor 1 thread/subsysteem en die laten synchroniseren met een barrier
hier dacht ik eerst ook aan, maar ik was bang voor de scalability. over een jaar of 10 hebben we misschien wel 256 cores, met een subsystem of 8 wordt het dan wat karig ;) maar later in je reactie zie ik hier oplossingen voor met een hybrid systemthread + workerthreads systeem, dus dit is een mogelijkheid :) het probleem dat je later aangeeft:
OpenGL (of op z'n minst publiek beschikbare wrapper implementatie?) bvb verwacht dat je alle calls vanuit 1 thread doet
had ik zelf ook al op hardhandige wijze ontdekt :P en dat los ik ook op de manier op (door opengl direct vanuit het graphics systeem aan te sturen ipv via de worker threads).

mijn internet was gistermiddag uitgevallen, dus een mooie tijd om je messaging uit te proberen. het leek me irritant om voor elke opdracht een subklasse te moeten afleiden, maar in de praktijk werkt het best lekker; het subclassen is even werk maar het aanroepen van een functie en returnvalues vragen is nu wel een eitje geworden. mooie oplossing dus! alleen loop ik tegen een praktisch probleem met de smart pointers aan, maar daar kom ik zo nog op terug.
Wil je hierin nog een stap verder gaan dan kan je publieke functies maken in je subsystem class die dan een message naar zichzelf zendt
dit is zeker een mogelijkheid. nu doe ik ongeveer hetzelfde, maar dan via de workerthreads: elk subsystem heeft een 'task' class, met een operator()() functie die als subsystem-task-thread draait, en die alle opdrachten naar de taskmanager(->workerthreads) stuurt, die ze vervolgens in haar eigen context uitvoert. de opdrachten roepen weer functies aan van de subsystemtask-class.

zoals je aangeeft is dat niet altijd efficient (via de workerthreads), dat kan ik nu wel mooi oplossen door in sommige gevallen de opdrachten niet naar de workerthreads te sturen maar direct naar de eigen thread. alleen kunnen die taken dan alleen singlethreaded worden uitgevoerd - maar ik neem aan dat je bedoeld dat dat in sommige gevallen juist efficienter is dan inherent singlethreaded subsysteem-subtaken via de workerthreads te doen? enige waar ik bang voor ben is dat de <numcores> workerthreads allemaal druk bezig zijn met iets, en dat een subsystem-thread vervolgens ook nog iets cpu gevoeligs gaat doen waardoor je meer drukke threads dan cores krijgt. kan dat niet problematisch zijn?

locks op de data zouden niet zo'n probleem moeten zijn; de meeste data wordt alleen gelezen, slechts sporadisch wordt er iets geschreven (posities/rotaties/matrices).

mijn vraag is nu dus vooral: wat is zwaarder:
- meer drukbezette threads draaien dan dat je cores hebt
of
- evenveel drukke threads als cores draaien maar alle messages vanaf subsystems moeten dispatchen naar de workerthreads

maargoed, dat is natuurlijk een mooi dagje profilen waardig ;) iig weer genoeg stof tot nadenken :)

nog even mijn praktische probleem dan..

ik heb je Message class ingebouwd, met de Handle, Execute, Wait etcetera, en wat subclassen voor de specifieke opdrachten. nu ziet het sturen van een opdracht er zo uit: (in dit geval een message direct naar een subsystem thread overigens)

C++:
1
2
3
4
SystemTaskMessage_StartFrame *startFrame = new SystemTaskMessage_StartFrame(thread);
thread->messageQueue.PushMessage(startFrame);
startFrame->Wait();
delete startFrame;


vanwege de pointer moet ik de Wait() doen, pas dan kan ik immers de message deleten. hier zou ik graag vanaf, en ik heb het geprobeerd met iets als:

C++:
1
2
3
boost::scoped_ptr<SystemTaskMessage_StartFrame> startFrame(new SystemTaskMessage_StartFrame(thread));
thread->messageQueue.PushMessage(startFrame);
startFrame->Wait();


alleen ontvangt de messageQueue.PushMessage gaarne een scoped_ptr<Message>, en omdat alle messages die ik daadwerkelijk stuur subklassen zijn gaattie zeuren dat ik een verkeerd type (scoped_ptr<SystemTaskMessage_StartFrame>) stuur. enig idee hoe ik dit kan oplossen?

nogmaals bedankt, ben iig nu weer een stuk verder dan gisterochtend :) ik zal eens gaan kijken naar dat Parallels framework - ik neem aan dat je dat bedoeld om van te leren, niet om daadwerkelijk te gebruiken? in dat laatste geval; ik zit te coden met gnu c++, dus dat wordt em niet ;)

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Je zal de shared_ptr moeten gebruiken. De scoped_ptr is vziw non-copyable.
C++:
1
2
3
4
5
6
7
// asynchroon
thread->msgQ.PushMsg(boost::shared_ptr<CMessage*>(new CSystemTaskMessage_StartFrame(parameters)));
//synchroon
boost::shared_ptr<CMessage*> msg(new CSystemTaskMessage_StartFrame(parameters));
thread->msgQ.PushMsg(msg);
// mogelijk hier nog andere dingen
msg->Wait();

Je kan mss ook een SendMsg() functie maken die Push()+Wait() doet. Dat bespaart je telkens de Wait() wanneer je ondertussen niets anders wil doen.

De reden waarom ik Parallels framework aanhaalde is omdat daar een behoorlijk slimme werkverdeling in plaats vindt. Wanneer er bvb al meer worker-threads bezig zijn dan er cores zijn dan kan de thread-pool scheduler beslissen om het nieuwe workitem dat binnenkomt te processen binnen de aanroepende thread ipv door te geven aan een threadpool thread. Je moet rekenen dat een workitem schedulen in een threadpool redelijk wat overhead inhoudt. Zie het als een functie inlinen of niet. Daarom is het altijd handig om zo groot mogelijk brokken uit te besteden. Kleine dingen doe je soms sneller zelf of stapel je op tot je een bulk aan data kan laten processen.

Je kan nu moeilijk optimaliseren voor een 256-core CPU zonder een goed framework voor multi-threading . Het is in veel gevallen al een uitdaging om voor meer dan 2/4 cores genoeg werk te vinden binnen 1 app.
Als je teveel in aparte threads stopt heb je teveel overhead. Stop je de verkeerde dingen in andere threads dan lopen ze voor elkaars voeten. Stop je er te weinig in dan heb je geen profijt. Je moet je dus dynamisch gaan aanpassen aan de situatie, maar als er simpelweg niet genoeg werk is wat je efficient kan uitbesteden dan zij het zo. Daarom zal het enorm belangrijk zijn om dmv profiling te kijken wat je wel en niet efficient aan een threadpool kan uitbesteden. Het voordeel van die messages is dan ook dat je van elk de Send()/Push() kan overriden om het werk in de huidige thread uit te voeren.

Dus begin eerst zonder/met beperkte threadpool. Ga dan dingen uitbesteden en profile elke stap.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

Verwijderd

Topicstarter
ach doh, had het noncopyable van de scoped ptr niet in verband gebracht met wat ik wilde doen..

heb het nu voor elkaar met de shared pointers, actieve messages en blockende primitieven, werkt als een tiet! voor ik het topic starte haalde ik met mijn testprogje (die alleen een swapbuffers message rondstuurde, zovaak mogelijk per seconde) ongeveer 220 fps. nu haal ik een solide 500 fps :D en het is allemaal een stuk duidelijker :)

heb erg veel aan dit topic gehad, (en weer een lading info er bij om nog te leren kennen) kan je/jullie niet genoeg bedanken!

binnenkort, als ik met het gamedata gedeelte bezig kan ('deel 2' van de enginestructuur) kom ik vast weer de nodige problemen tegen, jullie horen nog van me ;)
Pagina: 1