Asynchroon (II) bijv.nw. (comp.sci.) – synchroon (deel II)

In een vorige post van lang geleden verbaasde ik me over het gebruik van de term ‘asynchroon’ binnen de informatiseringswereld. Maar je bent nooit te oud om te leren (ik in elk geval nog niet) en ik heb net geleerd dat een deel van het vraagstuk hem erin zit dat er meer gebeurt dan wat je ouders je vroeger verteld hebben:

Bij multithreaded applicaties heb je in Java de mogelijkheid om bij methoden het keyword synchronized of annotatie @Synchronized mee te geven, en in .Net is er het attribuut [MethodImpl(MethodImplOptions.Synchronized)]. Twee threads kunnen dan niet tegelijkertijd die methode (of een andere gesynchronizede methode uit die klasse) uitvoeren. Soortgelijk kan je explicieter en ook nauwkeuriger met lock(myobject) { ... } in C# of synchronized(myobject) { ... } in Java aangeven dat een bepaald stuk code maar met een enkele thread tegelijk toegankelijk is.

Nu is dat niet het enige dat wordt bereikt: er gebeurt ook nog iets anders, en daarbij komt het woordt ‘synchroniseren’ wel in de normale betekenis in voor – namelijk ongeveer zoals in ‘twee horloges synchroniseren’.

Het geheugenmodel van Java (JLS hoofdstuk 17) en naar ik aanneem analoog ook de .Net CLR, specificeert namelijk dat een thread de door hem gebruikte variabelen mag cachen en dan met de gecachete waarden kan werken. Dit maakt het voor de compilers en VMs ook mogelijk om bepaalde optimalisaties door te voeren. De keerzijde is dat een wijziging aan een variabele daarmee dus niet direct zichtbaar is in andere threads omdat die in de thread-specifieke cache blijft hangen – en door die optimalisaties zelfs nooit zichtbaar hoeft te worden als je niet oplet.

Dat kan lastig zijn: als je bijvoorbeeld in thread A doet: while(!stopRequested) { ... }, en de variabele stopRequested wordt vanuit thread B op true gezet, dan blijft die waarde dus in de cache van thread B hangen; thread A ziet geen verschil en de lus blijft dus gewoon lekker doorlopen…

Het woord ‘synchronized’ blijkt hierop van invloed te zijn: bij het binnengaan en het verlaten van een synchronized block of methode is de JVM verplicht om de thread-specifieke cache eerst te synchroniseren met het main memory. Compilers en VMs mogen ook niet tijdens optimalisatie de instructies zodanig doorelkaar gooien dat instructies binnen het synchronized gedeelte voor (of na) de instructies uit dat blok plaatsvinden en andersom.

Op deze manier wordt gegarandeerd dat de state zoals die vlak voor het synchronized gedeelte (en ook weer vlak voor het einde ervan) in de thread aanwezig is, zichtbaar kan worden binnen andere threads. Natuurlijk is er nog geen directe garantie dat dat ook gebeurt: daarvoor moeten die andere threads ook weer eerst hun thread-specifieke cache synchroniseren met het main memory, bijvoorbeeld door een eigen synchronized block aan te roepen.

Dus vandaar dat zowel de getters als de setters van die variabelen gesynchroniseerd moeten worden: de setter, zodat bij het einde van het synchronized block thread B de nieuwe waarde van stopRequested naar het main memory schrijft, en de getter zodat thread A eerst de meest recente waarde uit dat geheugen opvist voordat hij de waarde ervan bekijkt.

Dat is althans een deel van de oplossing van het vraagstuk uit mijn eerdere posting. Waarom er in .Net dan ook steeds sprake is van Async dit en Async dat, zonder dat er per se synchronized blocks in het spel zijn, moet ik nog uit zien te vinden. Dus waarschijnlijk volgt over een hele poos nog wel weer een deel III van dit onderwerp.