Il·lustració del famós “problema dels filòsofs dinar”

Concurrency vs Loop d'esdeveniments vs Loop d'esdeveniments + Concurrency

En primer lloc, expliquem la terminologia.
Concurrència: significa que teniu diverses cues de tasques en diversos nuclis de processador / fils. Però és completament diferent de l’execució paral·lela, l’execució paral·lela no contindria una cua de tasques múltiples per a Paral·lel necessitarem 1 nucli / fil de CPU per tasca per a l’execució paral·lela completa, que en la majoria dels casos no podem definir. És per això que per a un desenvolupament de programari modern La programació paral·lela a vegades significa "concurrència", sé que és estrany, però òbviament és el que tenim de moment (depèn del model de cpu / thread de l'OS).
Loop d'esdeveniments: significa un cicle infinit roscat que realitza una tasca a la vegada i no només fa una cua de tasques única, sinó que també prioritza les tasques, ja que amb el bucle d'esdeveniments teniu un únic recurs per a l'execució (1 fil), per tal d'executar-lo. de seguida, per a prioritzar algunes tasques. En algunes paraules, aquest enfocament de programació s'anomena Programació segura de fil, ja que només es podria executar una tasca / funció / operació alhora, i si canvieu alguna cosa, ja es canviaria durant la propera execució de la tasca.

Programació simultània

Als ordinadors / servidors moderns tenim almenys 2 nuclis de CPU i mínim. 4 fils de CPU. Però als servidors ara és promig. el servidor té almenys 16 fils de CPU. Així que si escriviu un programari que necessiti una mica de rendiment, haureu de plantejar-lo de manera que s'utilitzin tots els nuclis de CPU disponibles al servidor.

Aquesta imatge mostra un model bàsic de concurrència, però no és tan fàcil que es mostri :)

La programació de concurrència s’està fent realment difícil amb alguns recursos compartits, per exemple, fem una ullada a aquest codi concurrent simple de Go.

// Concurrència incorrecta amb Go language
principal del paquet
importar (
   "fmt"
   "temps"
)
var SharedMap = make (map [string] string)
func changeMap (cadena de valor) {
    SharedMap ["test"] = valor
}
func main () {
    anar al canviMapa ("valor1")
    anar a ChangeMap ("valor2")
    time.Sleep (time.Millisecond * 500)
    fmt.Println (SharedMap ["prova"])
}
// Això imprimirà "valor1" o "valor2" que no sabem exactament.

En aquest cas, Go acomiadarà 2 feines simultànies probablement a diferents Nucli de CPU i no podem predir quina seria la primera d'executar, de manera que no sabríem què es mostrarà al final.
Per què? - És fàcil! Estem programant 2 tasques diferents a diferents nuclis de CPU, però utilitzen una sola memòria variable / compartida, de manera que tant canvien aquesta memòria, i en alguns casos, seria el cas d’excepció / excepció del programa.

Per predir l'execució de la programació de concurrència, hem de fer servir algunes funcions de bloqueig com Mutex. Amb ell, podem bloquejar aquell recurs de memòria compartida i fer-lo disponible només per a una tasca alhora.
Aquest estil de programació s’anomena Bloqueig perquè realment bloquegem totes les tasques fins que la tasca actual es fa amb memòria compartida.

La majoria dels desenvolupadors no els agrada la programació simultània, perquè la concurrència no sempre significa rendiment. Depèn de casos concrets.

Loop d'esdeveniments d'un sol fil

Aquest enfocament de desenvolupament de programari és molt més simple que la programació de concurrència. Perquè el principi és molt senzill. Només teniu una execució de tasca a la vegada. I en aquest cas no teniu cap problema amb les variables compartides / memòria, perquè el programa és més previsible amb una tasca única alhora.

El flux general segueix
1. Emissor d'esdeveniments afegint una tasca a la cua d'esdeveniments que s'executarà en un següent cicle de bucle
2. Obtenir el bucle d'esdeveniment de la cua d'esdeveniments i processar-lo en funció dels gestors

Escrivim el mateix exemple amb node.js

let SharedMap = {};
const changeMap = (valor) => {
    return () => {
        SharedMap ["test"] = valor
    }
}
// 0 El temps d'espera significa que estem fent una nova tasca a la cua per al proper cicle
setTimeout (changeMap ("valor1"), 0);
setTimeout (changeMap ("valor2"), 0);
setTimeout (() => {
   console.log (SharedMap ["prova"])
}, 500);
// en aquest cas Node.js imprimirà "value2" perquè és únic
// roscat i té "només una cua de tasques"

Com podeu imaginar en aquest cas de manera més previsible el codi de casos que amb l'exemple concurrent de Go, i és degut a que Node.js s'executa en un mode roscat únic mitjançant el bucle d'esdeveniments JavaScript.

En alguns casos, el bucle d'esdeveniments proporciona més rendiment que amb concurrència, a causa del comportament no bloqueig. Un exemple molt bo són les aplicacions de xarxa, perquè utilitzen recursos de connexió de xarxa única i processen dades només quan estan disponibles mitjançant bucles d'esdeveniments segurs de Thread.

Concurrency + Loop d'esdeveniments: piscina de fils amb seguretat de fil

Fer aplicacions només concurrents pot ser molt difícil, perquè hi ha errors de corrupció de la memòria a tot arreu o, simplement, l’aplicació començarà a bloquejar accions en cada tasca. Sobretot si voleu obtenir el màxim rendiment heu de combinar tots dos!

Donem una ullada al model de bucle de fil + esdeveniment d’esdeveniments de Nginx Web Server Structure

Worker Event Loop el realitza el processament principal de xarxa i configuració en un únic fil per a la seguretat, però quan Nginx ha de llegir algun fitxer o ha de processar les capçaleres / cos de sol·licitud HTTP, que bloquegen les operacions, envia aquesta tasca al seu grup de fils. per a processament simultani. I quan es fa la tasca, el resultat es remet al bucle d'esdeveniments per al resultat executat de processament segur de fil.

Així, utilitzant aquesta estructura, obteniu la seguretat del fil i la concurrència, la qual cosa permet utilitzar tots els nuclis de CPU per obtenir un rendiment i mantenir el principi de no bloqueig amb un bucle d'esdeveniments roscats.

Conclusió

Hi ha un munt de programari escrit amb concurrència pura o amb un bucle d’esdeveniments de roscat únic, però combinant ambdues aplicacions dins d’una sola aplicació, fent més fàcil escriure aplicacions performants i utilitzar tots els recursos de la CPU disponibles.