C/C++11 threading and memory model 06/08/2015

Har ägnat några dagar åt att tränga mig in i C11/C++11 och deras gemensamma memory model och stöd för multitrådning. Har en idé om att få till en kurs för bl.a. embeddedutvecklare inom ämnet.

Det är ett intressant ämne. Att ta en gammal standard som C med rötter från sjuttiotalet och deras syn på hur en dator förväntas fungera, och sedan, med bakåtkompabilitet, lyckas få till en standard där moderna datorer kan köra program och faktiskt relativt bra kunna utnyttja alla nya cores och kunna dölja bl.a. latency. Det krävs rätt mycket fingertoppskänsla!

Ytligt sett har man tagit 'best practice' med trådar och mutexes och standardiserat detta. Så inga större överraskningar där. Där det börjar bli intressant, det är hur man har vävt in det standarden kallar 'synchronizes with', 'release' och 'acquire' i det redan existerande ramverket.

En modern dator är väldigt hierarkisk om man ser till minnet. Vill man köra något väldigt fort, så ska man röra sig inom små minnesområden och absolut inte dela något. Så fort man behöver dela med sig av data till andra cores, börjar latency dyka upp. Det talar för att en moden dator skulle må bra av ett språk med klar åtskillnad mellan det egna minnet och det minnet man delar med andra (kanske ser en renässans för Erlang?). Man jobbar för fullt i det egna minnet och sedan skickar resultatet genom delat minne till nästa instans.

Samtidigt har C/C++ en platt minnesrymd. Allt minne är lika. Det var så det såg ut för 40 år sedan och för bakåtkompabilitet kommer det fortsätta se ut så tills vidare. Språkens minnesmodell är väldigt dåligt anpassade till de begränsningar hårdvaran har. Hårdvaran har fått anpassa sig. Så istället för lokalt minne med programmerarkontroll, har vi cachar som automatiskt mellanlagrar data och försöker upprätthålla illusionen av enhetligt minne.

En naiv standardisering hade tagit trådar och mutexes och stannat där. Eventuellt fått med atomics. Prestanda hade fått lida då de krav en modern minneshierarki ställer på språket är långt ifrån den platta värld språken föreskriver.

Det man har gjort istället är att ge en vink till faktum att en tråd när den kör utan behov av att kommunicera med omvärlden, kan köra på i isolering. Så grundinställningen är att begär man inte delning av data, så kan man behålla data i sina cachar. Delningen sker genom synkroniseringen. Vid skrivning sker ett 'release' event och på mottagande sida finns motsvarande 'acquire'.

Man kan betrakta det hela som ett 2 lagers system. Ett bottenlager där allt minne som är adresserbart finns. Sedan ett privat lager för varje enskild tråd. Vid exekvering kommer varje tråd ta in det data den behöver i sin cache. När den till slut är beredd att delge omvärlden sina resultat, utför den en 'release'. Det är en signal om att publisera det som skapats till det globala minnet.

På samma sätt, en tråd som är beredd att se vad som hänt i övriga systemet utför sin acquire, vilket indikerar att den ska uppdatera sitt lokala minne med vad som hänt i det stora. På detta sätt kan man skriva effektiv kod under programmerarens inflytande där enkeltrådad körning inte behöver lida så hårt av skillnaden mellan modell och hårdvara. Samtidigt kan man, till ett pris, synkronisera sig med övriga trådar och se vad som har hänt. Har man behov av prestanda kan man komma rätt långt med de svagare synkroniseringar som minnesmodellen erbjuder.

Som sagt, grundläggande modellen är 40 år så att det knakar i fogarna är förväntat. Tycker att det arbetet som lagts på att få ett användbart språk på modern hårdvara, har burit frukt.