Ilustrace slavného problému „Dinning filozofové“

Concurrency vs Event Loop vs Event Loop + Concurrency

Nejprve vysvětlíme terminologii.
Concurrency - znamená, že máte více front úloh na více jádrech / vláknech procesoru. Ale je to úplně jiné než paralelní provedení, paralelní provedení by neobsahovalo více front úloh pro paralelní případ, pro úplné paralelní provedení budeme potřebovat 1 jádro / vlákno CPU na úkol, což ve většině případů nemůžeme definovat. Proto pro vývoj moderního softwaru Paralelní programování někdy znamená „Concurrency“, vím, že je to divné, ale samozřejmě je to to, co v tuto chvíli máme (záleží na modelu CPU / vlákna OS).
Smyčka událostí - znamená jednovláknový nekonečný cyklus, který dělá jeden úkol najednou a neznamená to pouze vytváření jediné fronty úkolů, ale také upřednostňování úkolů, protože s smyčkou událostí máte pouze jeden prostředek k provedení (1 vlákno), takže k provedení u některých úkolů musíte okamžitě stanovit priority. Některými slovy se tento programovací přístup nazývá bezpečné programování vláken, protože najednou lze provést pouze jednu úlohu / funkci / operaci, a pokud něco změníte, bude již změněno během dalšího provádění úlohy.

Souběžné programování

V moderních počítačích / serverech máme alespoň 2 jádra CPU a min. 4 vlákna CPU. Ale na serverech nyní prům. server má alespoň 16 vláken CPU. Takže pokud píšete software, který potřebuje nějaký výkon, měli byste ho určitě zvážit tak, aby využil všechna jádra CPU dostupná na serveru.

Tento obrázek zobrazuje základní model souběžnosti, ale u corse není tak snadné, aby se zobrazoval :)

U některých sdílených zdrojů je programování souběžnosti opravdu obtížné, například umožňuje podívat se na tento jednoduchý souběžný kód Go.

// Chybná souběžnost s jazykem Go
hlavní balíček
import (
   "fmt"
   "čas"
)
var SharedMap = make (map [string] string)
func changeMap (řetězec hodnot) {
    SharedMap ["test"] = hodnota
}
func main () {
    go changeMap ("value1")
    go changeMap ("value2")
    time.Sleep (time.Millisecond * 500)
    fmt.Println (SharedMap ["test"])
}
// Tím se vytiskne „value1“ nebo „value2“, které nevíme přesně!

V tomto případě Go vypálí 2 souběžné úlohy pravděpodobně na různé CPU Core a nemůžeme předvídat, která z nich bude provedena jako první, takže bychom nevěděli, co se na konci zobrazí.
Proč? - Je to jednoduché! Plánujeme 2 různé úkoly na různá jádra CPU, ale používají jednu sdílenou proměnnou / paměť, takže obě tyto paměti mění a v některých případech by to bylo v případě havárie / výjimky programu.

Abychom předpovídali provádění souběžného programování, musíme použít některé uzamykací funkce, jako je Mutex. S ním můžeme zamknout tento prostředek sdílené paměti a zpřístupnit jej pouze pro jeden úkol najednou.
Tento styl programování se jmenoval Blokování, protože ve skutečnosti blokujeme všechny úkoly, dokud není aktuální úkol proveden pomocí sdílené paměti.

Většina vývojářů nemá ráda souběžné programování, protože souběžnost ne vždy znamená výkon. Záleží na konkrétních případech.

Single Threaded Event Loop

Tento přístup k vývoji softwaru je mnohem jednodušší než souběžné programování. Protože princip je velmi jednoduchý. Máte najednou pouze jeden úkol. A v tomto případě nemáte problém se sdílenými proměnnými / pamětí, protože program je lépe předvídatelný pro jeden úkol najednou.

Obecný tok následuje
1. Přidání úkolu do fronty událostí, který má být spuštěn v dalším cyklu smyčky
2. Smyčka událostí získání úlohy z fronty událostí a její zpracování na základě popisovačů

Umožňuje napsat stejný příklad s node.js

nechat SharedMap = {};
const changeMap = (value) => {
    return () => {
        SharedMap ["test"] = hodnota
    }
}
// 0 Timeout znamená, že vytváříme novou úlohu ve frontě pro další cyklus
setTimeout (changeMap ("value1"), 0);
setTimeout (changeMap ("value2"), 0);
setTimeout (() => {
   console.log (SharedMap ["test"])
}, 500);
// v tomto případě Node.js vytiskne „value2“, protože je jednoduchý
// vlákno a má „pouze jednu frontu úkolů“

Jak si v tomto případě dokážete představit, kódový kód je mnohem předvídatelnější než u současného příkladu Go, a to proto, že Node.js běží v režimu s jedním vláknem pomocí smyčky událostí JavaScriptu.

V některých případech poskytuje smyčka událostí vyšší výkon než při souběžnosti, protože není blokováno. Velmi dobrým příkladem jsou síťové aplikace, protože používají jediný prostředek síťového připojení a zpracovávají data pouze v případě, že jsou k dispozici pomocí smyček bezpečných událostí vlákna.

Concurrency + Loop Loop - Thread Pool with Thread Safety

Vytvoření souběžných aplikací může být velmi náročné, protože chyby poškození paměti by byly všude, nebo jednoduše vaše aplikace začne blokovat akce při každém úkolu. Obzvláště pokud chcete dosáhnout maximálního výkonu, musíte kombinovat oba!

Podívejme se na model Thread Pool + Loop Loop ze struktury Nginx Web Server Structure

Hlavní síťové a konfigurační zpracování zajišťuje Worker Event Loop v jediném vlákně pro bezpečnost, ale když Nginx potřebuje číst nějaký soubor nebo potřebuje zpracovat hlavičky / tělo HTTP požadavků, které blokují operace, odešle tuto úlohu do svého fondu vláken. pro souběžné zpracování. A když je úkol dokončen, výsledek je odeslán zpět do smyčky událostí, aby byl proveden výsledek bezpečného zpracování vlákna.

Díky této struktuře získáváte Thread Safety i Concurrency, což umožňuje používat všechna jádra CPU pro výkon a udržovat neblokující princip s jednovláknovou smyčkou událostí.

Závěr

Spousta softwaru je psána s čistou souběžností nebo s čistě jednovláknovou smyčkou událostí, ale kombinováním obou uvnitř jedné aplikace je mnohem snadnější zápis výkonných aplikací a využití všech dostupných zdrojů CPU.