Ansynchrone Programmierung und Event Loop
Browserprogramme sollen performant sein. Eine besondere Rolle spielt hierbei, dass der Benutzer eine Website bedienen kann, während diese lädt und andere Aktionen (parallel dazu) ausführt. Synchrone Programme können Aktionen nur nacheinander abarbeiten und währenddessen auf keine Eingaben reagieren; was das Bedienen einer Website erschweren würde.
Leider führt parallel ausgeführter Code immer auch zu mehr Komplexität. In der Webprogrammierung hat man deshalb folgende Möglichkeiten asynchronen Code zu strukturieren ausprobiert:
Callbacks
Futures
Promises
Async / Await
Hier ein Blogpost der sich mit der Historie von asynchronen Code in Javascipt beschäftigt https://developer.okta.com/blog/2019/01/16/history-and-future-of-async-javascript.
Schlussendlich wurden die async
und await
Keywords in einigen Sprachen eingeführt, so auch in Typescript.
async
und await
sind ein monadisches Sprachkonstrukt
und haben damit einen mathematischen Hintergrund aus der Typetheorie.
Monaden finden Anwendung in der Organisation von Seiteneffekten zu der restlichen Programmlogik.
So ist der Input und Output einer async
function in einem monadischen Kontext,
der in Typescript den Namen Promise
erhalten hat.
Üblicherweise wird in der asnyc
function ein Seiteneffekt gekapselt.
Seiteneffekte können hier das Warten auf die Antwort eines Requests oder auch IO im Allgemeinen sein.
Seiteneffekte sind auf ein Programm wirkende Umwelteinflüsse, z.B. Hardwarebegrenzungen, Zeit, etc..
Hier weiterführende Artikel:
https://en.wikipedia.org/wiki/Monad_(functional_programming)
https://en.wikipedia.org/wiki/Side_effect_(computer_science)
Dieses Programm soll verdeutlichen was die Keywords „async“ und „await“ bezwecken.
// Dieses Programm ist ein Zähler,
// der sich nach einer gewissen Zeit
// immer wieder erhöht.
// Eine function kann async sein.
async function delay(milliseconds: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function delayedCounting(pause: number, heading ) {
let num = 0;
while (true) {
await delay(400);
await delay(400);
await delay(400);
// await stößt die Ausführung der function an
// und wartet auf das Ergebnis der function.
// D.h. die aufeinanderfolgenden await Statements
// werden sequenziell nacheinander ausgeführt.
// Effektiv wird hier 3* 400 ms, also 1200 ms, gewartet
message = num.toString();
// auch der synchrone Code hier
// wird sequenziell nach dem letzten
// await Statement abgearbeitet.
heading.textContent = message;
console.log(message);
num +=1;
}
}
let message: string;
let heading = document.createElement('h1');
heading.textContent = message;
document.body.appendChild(heading);
// Normalerweise ist das await Keyword nötig,
// um eine async function auszuführen.
// Eine Ausnahme sind async functions,
// welche im top level eines Typescript
// Programmes aufgerufen werden,
// wie es hier der Fall ist:
delayedCounting(100, heading);
Zwei asynchrone Funktionen werden parallel ausgeführt:
async function slowCounting(pause: number, heading ) {
let num = 0;
while (true) {
await delay(400);
message = num.toString();
heading.textContent = message;
console.log(message);
num +=1;
}
}
async function delay(milliseconds: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, milliseconds);
});
}
let message: string;
let heading = document.createElement('h1');
let heading2 = document.createElement('h1');
heading.textContent = message;
heading2.textContent = message;
document.body.appendChild(heading);
document.body.appendChild(heading2);
Promise.all([
slowCounting(100, heading), slowCounting(100, heading2)
]);
Hier eine Referenz zu async
functions:
Die Event Loop
Typescript-Befehle werden in einer Event-Loop ausgeführt.
Dazu ist es wichtig zugrunde liegende Speichermodell im Browser zu verstehen. Der Speicher kann in drei Teile gegliedert werden:
Der Stack, der Ort an dem Funktionsparameter abgelegt werden.
Die Queue, hier werden alle Javascript Befehle vorgehalten, die es noch auszuführen gilt.
Der Heap, wo dynamische Speicherwerte abgelegt werden.
Was sich in der Queue befindet wird durch die Event-Loop abgearbeitet. Die Event-Loop nimmt nach dem FIFO Algorithmus ein Event aus der Queue. Das Event wird dann ausgeführt, es werden z.B. Funktionsparameter auf dem Stack abgelegt und abgearbeitet. Danach wird das nächste Element aus dem Stack genommen. Dieser Ablauf passiert sequenziell in einem Thread.
Eine Ausnahme können hier IO Aktionen darstellen. IO Aktionen müssen nicht die Event-Loop blockieren.
Hier ein Beispiel von einem blockenden Thread:
const syncWait = ms => {
const end = Date.now() + ms
while (Date.now() < end) continue
}
while (true){
syncWait(1000);
console.log("blocking");
}
Hier ein Beispiel von einem nicht blockenden Thread:
async function test() {
while (true){
await delay(100)
console.log("blocking");
}
}
async function delay(milliseconds: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, milliseconds);
});
}
test()