Uppgången, fallet och återkomsten av coroutines
Det här är andra delen av min artikelserie om nyheterna i Java 19. Det rör sig om väldigt nya nyheter, eftersom de utgör förhandsvisningar (preview) och även s.k. ruvande nyheter (incubating), d.v.s. kort och gott nya språkegenskaper som man håller på att utveckla och önskar synpunkter från Java programmerarna.
Så varför i all världen skriva om språknyheter i Java, som inte är helt färdigutvecklade ännu och det även lär dröja några versioner framåt innan de släpps för den stora publiken. Tja, det som händer nu är spektakulärt för Java som programspråk och kommer förändra sättet du skriver dina Java program på!
Java 19 släpptes förvisso härom veckan, men låt oss börja lite längre bak i tiden för att skaffa lite perspektiv och placera in den pågående språkevolutionen i ett större sammanhang.
I Begynnelsen var det Subroutine
När de första allmänna programspråken skapades på 1950-talet, såsom COBOL av Grace Hopper och FORTRAN av John Backus, fanns begreppet subroutine, vilket innebar att programexekveringen kunde hoppa till en annan plats i koden, utföra en uppgift och sen hoppa tillbaka och fortsätta där.
Språket COBOL saknade mekanismer för att överföra argument, utan man fick lägga all form av data i en global data-area kallad DATA DIVISION och sen genomföra en PERFORM SECTION för att hoppa till subrutinen. Språket FORTRAN hade en speciell avdelning för argumentöverföring kallad COMMON BLOCK och fungerande på ett liknande sätt, men med en aningen bättre flexibilitet.
1960-talets Hipsterspråk
Det är lite kul att ett språk som LISP, vilket lever vidare i nya hippa språk som Scheme och Clojure, skapades under samma tidsperiod av John McCarthy. De klassiska list-operationerna CAR och CDR, för att plocka ut huvud respektive svans på en lista, var uppkallade efter assembler-instruktionerna Content of Address Register och Content of Data Register.
Programspråkens Latin
Språket Algol, som skapades i början på 1960-talet, var kolossalt innovativt med bl.a. stack-exekvering, så som vi har nuförtiden. D.v.s. när en funktion anropas, läggs ett minnes block ovanpå det aktuella med utrymme för aktuella argument, återhopps-information samt plats för lokala variabler.
Begreppet Coroutine
Vid den här tiden börjande man fundera kring subrutiner, som det gick att hoppa ur och sen hoppa tillbaka in i subrutinen där man kom ifrån. Melvin Conway formulerade termen coroutine, för att beskriva den nya formen av exekvering. Bl.a. implementerade han stöd för detta i ett assemblerspråk.
Melvin har också fått sätta namn på det som kallas Conways lag och innebär att en applikationsdesign hos en större organisation tenderar att följa organisations-strukturen av densamma: "Organizations, who design systems, are constrained to produce designs which are copies of the communication structures of these organizations."
Simula
De två norska programmerarna Ole-Johan Dahl och Kristen Nygaard, skapade språket SIMULA under första halvan av 1960-talet. Språket var ämnat för simulerings-system, vilket också framgår av dess namn SIMUlation LAnguage.
De införde också språkmekanismer för klasser, objekt, arv, dynamisk (funktions-) bindning; det vi idag kallar för objekt-orienterad programmering så som det återfinns i språk som C++ och Java. Drygt femton år senare (1979) skulle dansken Bjarne Stroustrup emulera Simula i C, genom att skapa språket C++. Samt, att kanadensaren James Gosling, ytterligare cirka femton år senare (1995) skulle skapa ett förenklat C++, med språket Java.
Tillbaka till Simula; eftersom det var ett av de första språken som hade stöd för corutiner. M.h.a. mekanismer som detach
och resume
kunde man pausa och återuppta exekveringen av klassfunktioner.
Det finns ett intressant projekt på GitHub, som heter Portable Simula Revisited och utgör en portering av Simula till JVM:en, det är en transpiler som konverterar Simula-kod till Java-kod. Enligt dem själva: "The project was initiated as a response to the lecture held by James Gosling at the 50th anniversary of Simula in Oslo on 27th September, 2017."
Multitasking
Under 1960-talet och en bit in på 1970-talet implementerades corutiner i flera programspråk och corutiner användes för så vitt skilda ändamål som simulering och kompilatorer. Emellertid, insåg folk att det var kolossalt svårt att skriva program med corutiner och man började utvärdera mekanismer för att låta ett runtime-system automatiskt placera ut motsvarigheterna till detach och resume. Därmed var begreppet Cooperative Multitasking skapat.
Med tiden, vidareutvecklades detta till att byta exekvering efter en viss tidsperiod (timer-interrupt), vilket gav upphov till begreppet Preemptive Multitasking och process-begreppet, såsom vi känner det i moderna operativsystem.
Unix
Som en förenklad spin-off på MULTICS (MULTiplexed Information and Computing Systems), skapade Ken Thompson, Dennis Ritchie, Brian Kernighan med flera vid Bell Labs UNICS (UNiplexed Information and Computing System), där dess mest framträdande egenskap var just processer. Med tiden ändrade man stavningen till UNIX.
Erlang
Det skapades också flera programspråk med stöd för processer, där det mest kända är Ada (1980-talet). Varianter på processbegreppet i form av såväl cooperative och preemptive multitasking dök upp i olika programspråk och ramverk under 1980-talet. Ett av dessa var Actors-modellen, som var en stark inspirationskälla för Joe Armstrong med flera när han designade språket Erlang vid Ellemtel, som var ett forskningsinstitut i Älvsjö samägt av Ericsson och dåvarande Televerket.
Om du läst så här långt, undrar du säkert vad i hela friden har Erlang med modern Java att göra. Fortsätt läsa, så kommer du se hur.
Threads
I slutet på 1980-talet forskade man vid Carnegie Mellon University om hur processbegreppet kunde göras mer lättviktigt. Detta gav med tiden upphov till operativsystemet Mach och begreppet threads.
En av de första kommersiella OS implementationerna med stöd för threads var SUN OS. D.v.s. samma företag där James Gosling arbetade med att ta fram Java. Goslings ide om samarbetade uppgifter, var just threads och klassen java.lang.Thread
har funnits med från absolut första versionen av Java (1996).
Java Threads
Under de följande tjugo åren utvecklades Java till att bli det ledande språket för applikationsutveckling, hela tiden med grunden i ett väl formulerat system med trådar (threads), såsom vi fann dem i applikationsservrar såsom WebLogic, WebSphere, Jboss, Tomcat med flera.
Allt hade varit frid och fröjd om det inte var för två korrelerade faktorer. Det första var problemet med att skala upp kapaciteten för en större applikationen med tusentals samtidiga användare. Det andra, att det fanns en person som praktiskt formulerade en lösning på det första.
Begränsningen med Threads
Kapacitetsproblemet har att göra med att det helt enkelt inte går att skapa väldigt många trådar. Varje tråd representeras bl.a. av en anropsstack (call-stack) på typ 2-4 MB. I takt med att antalet trådar ökar växer minnesbehovet, vilket leder till större belastning på virtuell-minnes-systemet med betydligt fler s.k. page-faults (när en minnessida måste läsas in från hårddisken -swap partition- och en annan slängas ut). Fortsätter detta, så kraschar till slut hela operativsystemet.
Asynchronous Agent Router
Jag upplevde själv begränsningen med flertrådning i Java, då jag för drygt femton år sedan jobbade jag med prestanda-analys och realtids- monitorering av mycket stora Java applikationer. Företaget jag jobbade för sålde en analys- och monitoreringsprogramvara för Java system. Jag hade min bas i London, men jobbade ute hos kunder i hela Europa.
Hos en kund hade vi fyra monitorerings-servrar installerade på en AIX maskin med hundratals applikations-servrar, mestadels WebSphere, ansluta som pumpade realtidsdata hela tiden. Så kom det sig att vi behövde byta maskin till en ny med RedHat och uppgradera installationen till sex monitorerings-servrar.
Problemet för mig var peka om många hundratals installationer av vår monitorerings-agent, till den nya maskinen och till en utpekad server av de planerade sex. Det blir lätt dålig stämning om man bootar om en gigantisk internet-banks applikation, som betjänar kunder i varenda tidszon. Det är ju alltid dagtid någonstans.
Den idé jag fick var att skriva en proxy-router i Java, som emulerade de gamla servrarna och skickade trafiken vidare till servrarna på den nya maskinen. Min första version, designed på klassiskt vis med en tråd per logiskt koppel, d.v.s. agent metric stream, kraschade spektakulärt 😳, efter några hundra agent streams.
Lösningen blev att läsa in mig på och implementera systemet helt baserat på asynkron non-blocking I/O via klasserna i java.nio
. Dessa delar var tämligen nya år 2005 när detta begav sig.
Min helt asynkrona proxy-router fungerade ypperligt, samt att den knappt belastade den gamla maskinen den körde på. Tankegången var att kunden sen under en lägre tidsperiod kunde konfigurera om monitorerings-agenterna och migrera systemet stegvis. Emellertid, blev de så bekväm med lösningen att proxy-routern var i drift flera år senare.
Node.js
Under slutet på 2000-talet studerade Ryan Dahl olika system som inte hade de kapacitets-begränsningar som Java uppvisade. Bl.a. just Erlang, som hade fått stor spridning från åren 2005 och framåt, mycket p.g.a. att Joe Armstrong turnerade runt på de flesta konferenser om programspråk och pratade om Erlangs förträffligheter.
Så vad var hemligheten bakom Erlang's imponerande skalbarhet? Jo, alla former av I/O (in- och utmatning) var implementerade som asynkrona anrop till operativet (epoll
), detta i kombination med cooperative multitasking. I och med att Erlang realiserar loopar via svansrekursiva (tail-recursive) funktionsanrop, så funkar varje sådant anrop som en möjlig brytpunkt. Varje Erlang-process startar med ett heltal, vilket räknas ned efter varje anrop. När det kommer till noll, byts processen och talet återställs.
Här kan det nog vara på plats att påpeka att processer i Erlang inte på något sätt motsvarar processer i ett operativ. Utan en Erlang process fungerar mer som ett object med eget driv, eller om man säger corutin.
Ryan behövde applicera sina tankegångar på ett språk som helt saknade stöd för fler-trådad exekvering och det blev JavaScript, vilket skapades av Brendan Eich femton år tidigare. Resultatet blev Node.js (2009) och i kölvattnet av detta hela den frenesi kring asynkron exekvering vi just nu upplever i de flesta programspråk. Jag har jag skrivit detta i tidigare artiklar.
CompletableFuture
När Java 8 släpptes 2014, fanns bland annat en monstruös klass med namnet java.util.concurrent.CompletableFuture<T>
med närmare 70 medlemsfunktioner! Denna var ett helt misslyckat försök till att hänga med på "asynkrontåget" Ryan hade startat.
Generator Functions i JavaScript
Det stora intresset för Node.js skapade också ett starkt omvandlingstryck på språket JavaScript (EcmaScript). En essentiell men mindre uppmärksammad språkegenskap i Modern JavaScript är generator-funktioner (function*
), såsom.
function* fibonacci(N) {
let [f0, f1] = [0, 1];
while (N-- > 0) {
yield f1;
[f0, f1] = [f1, f0+f1];
}
}
//for (let f of fibonacci(10)) console.log(f);
console.log(Array.from(fibonacci(10)))
Kör man programmet, kan det se ut så här:
JS> node fibonacci.js
[ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
Notera anropet av yield
; ty där har vi den moderna formen av detach
😮! Generator-funktioner i JavaScript representerar coroutines och ligger till grund för hur async functions i ES2017 realiseras.
async getData(url) {
const response = await fetch(url);
if (response.ok) return await response.json();
throw new Error('Bummer');
}
Coroutines i Moderna Språk
När vi nu ser stöd för asynkron exekvering rullas ut i flera programspråk, så är det just i form av corutiner. Vi fick det nyligen i C++20. Det finns i JVM-språket Kotlin och t.ex. GO (kallas då för goroutines 😎)
Virtual Threads
Tillbaka till där vi startade i denna exposé över programspråks-utvecklingen. Projekt Loom, som nu gradvis realiseras i Modern Java bygger på virtuella trådar (virtual threads). En virtuell tråd realiseras som en corutin, men denna gång helt utan att vi behöver hantera detach/resume
på egen hand, utan mer i Node.js stil. Detta innebär att alla väntande anrop (blocking calls) hanteras av JVM:en och utför motsvarigheten till detach
, eller med ett mer modernt uttryck await
.
Så sammanfattningsvis; på 1960-talet blev corutiner populärt, för att under 1970-talet hamna i "vanrykte" och vara bortglömt under 1980-talet och framåt, till förmån för trådar under 1990-talet och 2000-talet. För att sedan återuppstå under 2020-talet, när klassisk multi-trådning kommit till vägs ände.
I nästa artikel kommer jag skriva om virtuella trådar i Modern Java. Häng med!