Probabilmente è meglio pensare al modello dell'attore come antenato della programmazione orientata agli oggetti; e confrontalo con OOP. Tuttavia, poiché (spero) tu abbia già familiarità con OOP, farò il contrario.
Per OOP, immagina di avere un oggetto semplice con codice vagamente simile a questo:
integer currentValue = 0;
void incrementMethod(void) {
currentValue = currentValue + 1;
}
void decrementMethod(void) {
currentValue = currentValue - 1;
}
integer getValueMethod(void) {
return currentValue;
}
void setValueMethod(integer value) {
currentValue = value;
}
Questo ha problemi con la concorrenza, perché più thread / CPU possono eseguire i metodi contemporaneamente e rovinarsi l'un l'altro. La soluzione del "modello attore" consiste nel prevenire l'accesso diretto ai metodi e fornire l'isolamento tra "cose" (oggetti / attori). La conseguenza è che hai bisogno di una qualche forma di comunicazione tra le cose isolate. Quella comunicazione è il passaggio dei messaggi.
L'esempio stupido di cui sopra potrebbe diventare:
integer currentValue = 0;
void main(void) {
while(running) {
message = getMessage();
switch(messsage->type) {
case INCREMENT_REQUEST:
incrementMethod();
break;
case DECREMENT_REQUEST:
decrementMethod();
break;
case GET_VALUE_REQUEST:
getValueMethod(message->senderID);
break;
case SET_VALUE_REQUEST:
setValueMethod();
break;
default:
sendMessage(senderID, UNKNOWN_REQUEST, NULL);
}
}
}
void incrementMethod(void) {
currentValue = currentValue + 1;
}
void decrementMethod(void) {
currentValue = currentValue - 1;
}
void getValueMethod(messagePort senderID) {
sendMessage(senderID, GETVALUE_REPLY, currentValue);
}
void setValueMethod(integer value) {
currentValue = value;
}
Poiché la messaggistica serializza le richieste e poiché nessun dato è condiviso tra thread, non è necessaria alcuna altra forma di controllo della concorrenza. Poiché nessun dato è condiviso tra thread, è anche altamente distribuibile (ad es. Puoi avere un sistema in cui diversi attori si trovano su computer fisici diversi). A causa dell'isolamento tra attori è "più possibile" (almeno in teoria) supportare cose come l'aggiornamento live (ad esempio sostituendo il codice di un attore mentre il resto del sistema è in esecuzione) e la tolleranza di errore (ad es. Tripla ridondanza con 3 attori su 3 computer diversi invece di uno). Infine, poiché un attore è in grado di accettare una richiesta sconosciuta, è più estensibile (ad esempio potresti inviare un messaggio di ADD_VALUE_REQUEST
a un attore che non capisce, e poi fare in modo che l'attore capisca quel tipo di messaggio in seguito).
Il problema principale con (l'implementazione ingenua di) il modello di attore è la prestazione - con un thread per attore più il sovraccarico del messaggio che passa la performance (rispetto a OOP) sarebbe estremamente negativo. Esistono diversi modi per risolverlo, ma i metodi più comuni sono l'uso di molti attori per thread e per consentire agli attori all'interno della stessa discussione di chiamare direttamente i rispettivi metodi (ed evitare che il messaggio passi in quel caso). Con questo in mente, è possibile definire le origini di OOP come "modello attore con un solo thread in cui tutti gli attori possono chiamare direttamente i rispettivi metodi" (realizzando che il multi-threading è stato riadattato su OOP in seguito).
L'altra cosa che devo sottolineare è che per il mio esempio sono intenzionalmente "di basso livello" per aiutarti a capire cosa sta succedendo dietro le quinte. Per un linguaggio progettato per il modello attore, il ciclo di gestione dei messaggi mostrato sopra ( main()
nella sua interezza) sarebbe implicito e non esplicito; per salvare il programmatore dal digitare così tante cose, e anche in modo che il compilatore possa fare le ottimizzazioni che ho citato.