Effektiv dataöverföring mellan trådar

Permalänk
Hedersmedlem

Effektiv dataöverföring mellan trådar

Sitter och klurar på ett problem jag inte får någon ordning på. Jag skall så småningom implementera detta i C(pthreads) men experimenterar just nu i Java för att det är lättare.

Situationen är följande:

- Jag har två trådar, där information skall skickas från tråd 1 till tråd 2, och tråd 2 skall *väckas* när den fått informationen

- Tråd 1 pollar en extern buffert och får information därifrån.
- Tråd 2 skall reagera på när den får information från Tråd 1.

Det är inte så svårt att fixa en naiv trådsäker lösning (tror jag, gemensam synkad buffert som tråd två kopierar ifrån), MEN, jag vill hitta en lösning där jag slipper låsa (mutex lock/unlock eller semaphores) mer än ett väldigt begränsat antal gånger och som dessutom inte kräver några kopieringar.

Min tanke är att skapa 2 mellanliggande buffrar, och helt enkelt bara skifta pekare mellan dessa när det passar.

Låt oss nu säga att vi har två buffertpekare (p1 och p2) som initiellt går tilldelats buffertaddresser (a1 och a2).

p1 = a1 (initiellt)
p2 = a2 (initiellt)

Min tanke är att Tråd1 skriver fritt till p1, och tråd 2 läser fritt från p2, när tråd 2 märker att p2 är tom så går tråd 2 och lägger sig, och ev meddelar/flaggar att tråd 1 nu är tillåten att swappa adresserna som p1 och p2 lagrar.

Problemet är ju att även om tråd 1 kör swap(p1,p2) så kan kompilatorn optimerat bort att tråd 2 känner av detta pga tråd-specifik cachning. Eftersom nyckelordet "volatile" inte verkar vara generellt accepterat av alla kompilatorer och hårdvaruarkitekturer så blir jag som jag förstått det tvungen att använda pthreads egna funktioner som garanterar att båda trådarna verkligen läser från minnet och inte sin egen tråd-cache när dom skall. Men det är ju just det som är problemet.

Jag vet hur jag ska göra för att åstadkomma att pekaradress-swappen som görs av tråd 1 uppdaterar p1 och p2-adresserna hos både tråd 1 och tråd 2. MEN. Tråd 2 kan ju fortfarande ha cachat informationen som ligger i både a1 och a2, eller?

Alltså, förtydligande av problemet. Tråd två känner från början till datan som finns på adress a1 och a2. När Tråd två sedan blir tillsagd att läsa av datan på a1 och a2 igen fattar inte kompilatorn att innehållet där kan ha ändrats utan den läser bara samma gamla skräp som cachats tidigare. Eller? (obs p1 och p2 kan vara void* och innehålla data på N byte)

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Hedersmedlem

boost::lockfree kanske kan vara något (Jag har dock inte testat själv)?

Permalänk
Hedersmedlem
Skrivet av Elgot:

boost::lockfree kanske kan vara något (Jag har dock inte testat själv)?

Verkar bra men ser ut att vara C++?
Nämnde kanske inte att det jag gör enbart får vara C :/

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Hedersmedlem
Skrivet av 'Gi:

[Gurra;10989236']Verkar bra men ser ut att vara C++?
Nämnde kanske inte att det jag gör enbart får vara C :/

Ah, jo det gjorde du ju faktiskt.

Permalänk
Medlem

Du kan köra en ringbuffer där du skriver. Om det är single write single consumer bör du enkelt i C bara kunna hålla reda på läs/skrivpositioner och spinna tills det finns något att läsa eller tillräckligt med utrymme för att skriva. För kommunikation av läs/skrivpositioner kan du endera köra atomics eller bara skriva positionerna direkt. Skriver du all data och data-header (storlek etc) till ringbuffern innan du markerar den som valid bör det inte vara några problem, eftersom de flesta arkitekturer (x86/PPC) garanterar att skrivningar av vissa storlekar (32bit t.ex.) är atomiska.

Visa signatur

void@qnet
teeworlds, stålverk80, evil schemer, c, c++
Languages shape the way we think, or don't.

Permalänk
Hedersmedlem
Skrivet av jdv:

Du kan köra en ringbuffer där du skriver. Om det är single write single consumer bör du enkelt i C bara kunna hålla reda på läs/skrivpositioner och spinna tills det finns något att läsa eller tillräckligt med utrymme för att skriva. För kommunikation av läs/skrivpositioner kan du endera köra atomics eller bara skriva positionerna direkt. Skriver du all data och data-header (storlek etc) till ringbuffern innan du markerar den som valid bör det inte vara några problem, eftersom de flesta arkitekturer (x86/PPC) garanterar att skrivningar av vissa storlekar (32bit t.ex.) är atomiska.

Ok, jag kanske har missuppfattat vad atomiska läsningar/skrivningar innebär.
Som jag förstått det innebär det att en atomisk skrivning att hela datan som skall skrivas görs i ett svep, utan att någon kan komma in emellan och läsa en halvt färdigskriven klump (dvs en annan läsande tråd kan inte av misstag komma och läsa när bara *hälften* har skrivits). Att bara en får läsa/skriva i taget löses enkelt med pthreads mutex lock/unlock, så den delen är inget problem.

Men atomicitet garanterar väl inte att läsningen/skrivningen inte cachas lokalt för 1 tråd? Och nu pratar jag inte om reordring/omordning av instruktionerna utan att datan kan skrivas atomiskt till en lokal cache (för ena tråden) och inte till den faktiska *target-adressen i huvudminnet* som den andra tråden förväntas läsa. Och på samma sätt så kan läsande tråden läsa ur sin cache istället för från huvudminnet. Som jag förstått det är det precis här man bör använda *volatile*. Sedan googlar jag på det och får i princip svaret "använd aldrig volatile" :P...

Men, låt oss säga då att två trådar delar på en gemensam buffert, typ en int * (allokerad med typ int * data = malloc(sizeof(int)*N) ; låt oss säga N element maximalt). Låt det vara en buffert med tillhörande indikationer för aktuella läs/skrivpositioner. Antag att positionerna kan delas perfekt mellan trådarna utan bekymmer. Hur garanterar jag att tråd 2 inte läser från sin tråd-cache utan från adressen 'data' ?
Räcker det att köra int volatile * data = malloc(sizeof(int)*N); ? Garanterar det att hela datablocket är volatile och fattar dagens kompilatorer vad jag menar ?

Annars kan man ju tänka sig deklarera en struct typ

struct buffer {
volatile int status;
volatile int rpos;
volatile int wpos;
volatile int data[1000];
}

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Medlem

Volatile markerar bara att kompilatorn inte får cacha värdet i ett register, utan måste hämta det från minne (eller cache) varje gång det ska användas. Du behöver alltså bara ha det när du ska spinna för att se om nästa data är klar, eller när du ska se om det finns plats i buffern. Din tanke är bra men du behöver inte ha volatile på så många ställen du tror

EDIT: du kan ju börja med att skriva det med ett datafält i början. Ena tråden skriver data, spinner på att det är färdigbehandlat, skriver nytt osv. Andra väntar på data, läser, markerar att den är klar. Det är några edge cases med i vilken ordning du måste läsa/skriva minne där, och vilka variabler som måste vara volatile var, så det är nog en enkel övning att börja med.

Visa signatur

void@qnet
teeworlds, stålverk80, evil schemer, c, c++
Languages shape the way we think, or don't.

Permalänk
Medlem

Problemet med att inte använda synkroniseringsprimitiver är att man kommer slösa cpu cyklar med att bara spinna och kolla ifall det finns någon data. Det är för lite information i inlägget för att avgöra vad den bästa lösningen är, då det finns ett antal olika aspekter man måste ta hänsyn till.

Det finns ett antal designmönster man skulle kunna anpassa för detta problem, som t.ex. Leader/followers, dock så kräver de flesta någon form av synkronisering.
http://www.kircher-schwanninger.de/michael/publications/lf.pd...

Visa signatur

Intel Core i7-3770K | NVIDIA Geforce GTX 980 | 16 GB DDR3 | DELL P2415Q | DELL U2711 | DELL U2410

Permalänk
Medlem
Skrivet av MagnusL:

Problemet med att inte använda synkroniseringsprimitiver är att man kommer slösa cpu cyklar med att bara spinna och kolla ifall det finns någon data. Det är för lite information i inlägget för att avgöra vad den bästa lösningen är, då det finns ett antal olika aspekter man måste ta hänsyn till.
[/url]

Absolut. Det blir alltid en avvägning.

Visa signatur

void@qnet
teeworlds, stålverk80, evil schemer, c, c++
Languages shape the way we think, or don't.

Permalänk
Hedersmedlem
Skrivet av jdv:

Volatile markerar bara att kompilatorn inte får cacha värdet i ett register, utan måste hämta det från minne (eller cache) varje gång det ska användas. Du behöver alltså bara ha det när du ska spinna för att se om nästa data är klar, eller när du ska se om det finns plats i buffern. Din tanke är bra men du behöver inte ha volatile på så många ställen du tror

EDIT: du kan ju börja med att skriva det med ett datafält i början. Ena tråden skriver data, spinner på att det är färdigbehandlat, skriver nytt osv. Andra väntar på data, läser, markerar att den är klar. Det är några edge cases med i vilken ordning du måste läsa/skriva minne där, och vilka variabler som måste vara volatile var, så det är nog en enkel övning att börja med.

Nu är jag ganska ny på vissa saker så vad som menas med "spinner" vet jag inte, men gissar att du hade tänkt dig en implementation som "while(...) readIfChanges shortSleep endWhile" eller något sånt

Det är just cachningen jag måste få bort.

tanken är något sånt här

Lock lock; int * globalData; Tråd 1: void mainLoop1() { parsa och hantera data från extern källa lås(locK); kopiera in resultat till globalData öppna(lock); } int * tråd2LocalBuffer; void mainLoop2() { lås(locK); kopiera från globalData till tråd2LocalBuffer; öppna(lock); jobba fritt med tråd2LocalBuffer. }

Skall 'globalData' här vara volitile för att garantera att trådarna här verkligen alltid (det får inte tappas en enda gång) verkligen skriver och läser från globalData och inte lokalcache.

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Medlem

Nu ska jag inte vara sådan, men finns det någon anledning att inte kopiera från extern källa till tråd2LocalBuffer direkt? Hur stora chunks kommer du läsa skriva? Jag beskrev en lockless-variant som ger låg latency. Med lås är det risk för högre latency, men den skilnaden försvinner ju större datachunks du flyttar (kopieringen tar längre och längre tid).

Visa signatur

void@qnet
teeworlds, stålverk80, evil schemer, c, c++
Languages shape the way we think, or don't.

Permalänk
Hedersmedlem
Skrivet av jdv:

Nu ska jag inte vara sådan, men finns det någon anledning att inte kopiera från extern källa till tråd2LocalBuffer direkt? Hur stora chunks kommer du läsa skriva?

Den externa källan är abstrakt och i vissa fall t.ex. en socket som skall tömmas och data parsas/tolkas/jobbas på innan tråd 2 ska förmedla den. Själva behandlingen av datan måste skötas i en separat tråd för att resten av systemet skall fungera. Tråd 2 kan även få andra uppgifter att ta hand om emellan som tar tid.

Poängen är att tråd 2 ska vara fri att behandla sin lokala buffert samtidigt som tråd 1 är fri att fylla på den globala.

EDIT: Inverkan av låsen på prestandan är inte viktig i det här exemplet. Jag är medveten om dess tröghet då jag kodat flera liknande saker i Java. I denna tillämpning handlar det om relativt mycket (i tid sett, så kostnaden för trådlåsen är minimal i förhållande) behandling av varje dataklump, även om klumparna nödvändigtvis inte är så jättestora.

Det enda viktiga är att jag kan garantera att inte sakerna på något sätt ..ehm..ballar ur
Så att när någon tråd verkligen skriver eller läser från global-buffert, då MÅSTE dom kolla i verkliga minnet och inte trådlokalt optimeras bort. Garanterar volatile det?

min tolkning av http://publications.gbdirect.co.uk/c_book/chapter8/const_and_... är att det faktiskt är precis det som volatile gör.

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Medlem

Ingår det i problembeskrivningen att skydda sig från caching som om detta inte sköttes per automagi? Vanligtvis implementeras skydd för detta i hårdvaran, så du inte ska behöva tänka på det. Och när hårdvarans skydd inte är helt fel-säkert så brukar kompilatorn sköta det åt dig. Poängen är, om du inte använder någon skum hårdvara/kompilator, så bör du aldrig behöva tänka på cachen om du inte vill få programmet så snabbt som möjligt. Det kan tex hända att en tråd skriver någonstans och den andra läser i närheten, då kommer den andras cache att invalideras och det tar en massa onödig tid att fixa det. Då skulle man kunnat lagt informationen lite längre ifrån varandra i minnet.. Men vad gäller att hålla informationen "korrekt" mellan trådar så kommer det skötas av hårdvara/kompilator.

Visa signatur

"Some poor, phoneless fool is probably sitting next to a waterfall somewhere, totally unaware of how angry and scared he's supposed to be." - Duncan Trussell

Permalänk
Hedersmedlem
Skrivet av gibbon_:

Ingår det i problembeskrivningen att skydda sig från caching som om detta inte sköttes per automagi? Vanligtvis implementeras skydd för detta i hårdvaran, så du inte ska behöva tänka på det. Och när hårdvarans skydd inte är helt fel-säkert så brukar kompilatorn sköta det åt dig. Poängen är, om du inte använder någon skum hårdvara/kompilator, så bör du aldrig behöva tänka på cachen om du inte vill få programmet så snabbt som möjligt. Det kan tex hända att en tråd skriver någonstans och den andra läser i närheten, då kommer den andras cache att invalideras och det tar en massa onödig tid att fixa det. Då skulle man kunnat lagt informationen lite längre ifrån varandra i minnet.. Men vad gäller att hålla informationen "korrekt" mellan trådar så kommer det skötas av hårdvara/kompilator.

Hårdvaran är absolut inte garanterad att vara x86 eller ens liknande. Det kan vara något som sitter i ett gammalt kylskåp och tickar med 10 olika cache-nivåer :).

Hastigheten för själva minnesaccessen är i princip ingenting jämfört med vad trådarna annars gör, det är inga jättehöga uppdateringsfrekvenser utan rätt stora chunks. Det enda viktiga är att dom absolut aldrig får läsa ur sin egen cache för globalbuffern utan måste gå och hämta från och skriva till det *riktiga* minnet. Jag vill kunna garantera detta i koden, och undrar därför om volatile skulle göra det. Enligt min länk ovan så bör så vara fallet. Får väl höra med kollegorna när dom dyker upp om dom kan hjälpa annars.

Ingår det i problemställningen att skydda sig mot cachad läsning och skrivning till just den variabeln? - Oh yes!
Utgå ifrån att just det minnesområdet som läses och skrives av båda trådarna måste läsas och skrivas helt, varje gång.

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Medlem

Kanske därför du inte får de svar du vill, de flesta här antar nog att det inte är en konstig motorola-cpu inbakad i en fpga eller liknande som används. På rak arm har jag inget svar på den frågan då, intressant.. Ska kluras på.

Visa signatur

"Some poor, phoneless fool is probably sitting next to a waterfall somewhere, totally unaware of how angry and scared he's supposed to be." - Duncan Trussell

Permalänk
Hedersmedlem

Tänk om allt vore enkelt som i Java, där implicerar varje synchronized(...) {}-block att alla variabler som läses eller skrives inuti körs mot back-end-minnet

Visa signatur

Every time you create an iterator: God kills a kitten.

Permalänk
Medlem

Ju mindre och ju mer embeddad CPU du kör desto lager minne/cache lär du få dock. Vad är det för CPU? Och om det nu är så embeddat, har du hårdvarutrådar?

Visa signatur

void@qnet
teeworlds, stålverk80, evil schemer, c, c++
Languages shape the way we think, or don't.

Permalänk
Skrivet av 'Gi:

[Gurra;10989647']Så att när någon tråd verkligen skriver eller läser från global-buffert, då MÅSTE dom kolla i verkliga minnet och inte trådlokalt optimeras bort. Garanterar volatile det?

Volatile är information till kompilatorn att den ska skriva ut värdet till minnessystemet, som består av cache och RAM-minne, och inte hålla det lagrat i något register eller på stacken. Det låter som att du vill använda det, men det beror som sagt helt på vad du tänker implementera.

Minnessystemet i sin tur ger garantier om ifall alla hårdvarutrådar kommer att ha en konsistent bild av minnet. Det implementeras i datorn med någon form av cache koherens. Om du söker på "shared memory consistency model" så kommer du hitta mer information om det. På x86 ges starka garantier om konsistens så där borde du inte ha några problem, men på t.ex. SPARC finns det specifika instruktioner för att synka skrivningar till minnet (membar #Sync).

Permalänk
Hedersmedlem

Ok det visade sig att vi kommer använda ett väldigt speciellt OS på vad som förmodligen kan klassas som någon udda fpga (f.ö. ett ämne jag kan nada om), MEn, med den fördelen att vi kommer köra det mesta singeltrådat i början, och om det blir flera trådar någonstans så är det något vi kör på några för systemet specifika trådbibliotek.

So... problem solved antar jag , men bra information där på slutet av jdv och VirtualIntent, man tackar. Det kan säkert komma till användning i framtiden. tänk om man kunde övertyga kollegorna om att dom kunde ta och skriva en jvm till hårdvaran :P.

Visa signatur

Every time you create an iterator: God kills a kitten.