Programmazione Parallela in PHP

Programmazione Parallela in PHP? Non è follia… e non è nemmeno complesso: tutto è già studiato per essere parallelizzato… come?

Lo vedremo leggendo queste pagine!

P.S.: l’esperimento utilizza alcuni escamotage inediti: da mie ricerche su internet, nulla è stato mai sperimentato prima in PHP.

I thread

Windows (come Linux), nativamente supporta i processori multipli utilizzando i thread.

Cosa è un thread?

Un thread lo semplificheremo come un insieme di istruzioni che possono essere eseguite da un singolo processore; Windows (supportando il multithread e hardware multiprocessore), la cosa più logica che possa fare quando ha più thread da eseguire in contemporanea è quella di assegnarli a processori diversi: in tal modo il carico è diviso sui processori disponibili.

Nel caso in cui i thread siano più di uno su un singolo processore, i threads non potendo essere eseguiti contemporaneamente vengono eseguiti dal processore a circolo continuo per brevissimi intervalli di tempo; in realtà il meccanismo è più complesso, infatti prevede anche il fatto che ci possa essere una priorità impostata per ogni thread: un thread con maggiore priorità utilizzerà per più tempo il processore rispetto un thread a priorità minore.

Da quanto detto si capisce come un thread perde la possibilità di utilizzo del processore anche se non ha finito il suo compito: continuerà la serie di istruzioni che stava eseguendo quando sarà nuovamente il suo turno.

Ambiente di sviluppo

PHP è un linguaggio lato server che gira (o almeno noi lo faremo girare) sulla piattaforma WAMP: Windows, Apache, MySql e PHP.

Non mi prolungherò molto sul Apache e PHP in quanto si suppone che chi legga un articolo sulla programmazione parallela in PHP conosca già l’ambiente del quale si sta parlando.

Apache è progettato in modo da creare un nuovo thread quando deve servire una richiesta effettuata da un client; per ogni client che fa una richiesta infatti, genera un thread, che puo essere assegnato (nel caso di più processori) a processori diversi (se ne occuperà il sistema operativo).

Accenni di programmazione parallela

Una singola persona non potrà mai fare una addizione ed una moltiplicazione contemporaneamente (provateci, non vi riuscirete!): è la stessa identica cosa per un processore, solo che lui è molto più veloce ed anche se esegue le operazioni una di seguito all’altra, sembra che esse vengano eseguite contemporaneamente.

Avendo due persone, se ad uno gli si dice di fare la moltiplicazione e ad un altro gli si dice di fare la somma, queste ultime, invece, possono eseguire le operazioni contemporaneamente: è la stessa identica cosa per due processore distinti.

Come rendere il software parallelo

Per una operazione elementare chiaramente è impossibile, ma per un operazione complessa che è possibile scomporre in due operazioni “meno complesse” ciò è possibile.

Un esempio (matematico) concreto:

(SQRT sta per square root, radice quadrata).

SOMMATORIA(SQRT(n)) con n che varia da 1 a 100.000.000 a passi di 1.

Dando per scontato che la radice quadrata sia un operazione elementare (per un processore), se avessimo a disposizione un processore per eseguire l’operazione ci staremmo un tempo T, se avessimo a disposizione due processori, parallelizzando l’algoritmo, per eseguire l’operazione ci staremmo un tempo T/2.

Perchè?

Ovviamente, dobbiamo parallelizzare l’operazione, se eseguiamo un thread che calcola:

SOMMATORIA(SQR(n)) con n che varia da 1 a 100.000.000, il tempo impiegato sarà T anche se i processore della macchina sono due: infatti il sistema operativo non è in grado di capire che l’operazione può essere parallelizzata e non può creare due thread da assegnare a due processori (ne utilizzera 1 al 100%).

La parallelizzazione deve essere preventivata e progettata dallo sviluppatore dell’applicazione.

Come facciamo a parallelizzare l’operazione?

Molti l’avranno già intuito:

SOMMATORIA(SQRT(n)) con n che varia da 1 a 100.000.000

E’ equivalente a scrivere:

SOMMATORIA(SQRT(n)) con n che varia da 1 a 50.000.000: thread 1, processore A
sommato a (+)
SOMMATORIA(SQRT(n)) con n che varia da 50.000.001 a 100.000.000: thread 2, processore B

Obbiezione: calcolare la radice quadrata di 1 sarà sicuramente più semplice che calcolare la radice quadrata di 50.000.000 quindi il thread 1 impiegherà molto meno tempo ad essere eseguito del thread 2: cioè è vero nel caso in cui la radice quadrata non sia un operazione elementare;

Supponendo che essa non sia un operazione elementare potremmo scrivere:

SOMMATORIA(SQRT(n)) con n che varia da 1 a 100.000.000: thread 1, processore A

E’ equivalente a scrivere:

SOMMATORIA(SQRT(n)) con n che varia da 1 a 99.999.999 ad intervalli di 2 (quindi gli n varieranno con questa successione 1,3,5,7,9,11…99.999.999 )
sommato a (+)
SOMMATORIA(SQRT(n)) con n che varia da 2 a 100.000.000 ad intervalli di 2 (quindi gli n varieranno con questa successione 2,4,6,8,10,12…100.000.000)

In tal modo l’obbiezione precedentemente esplicitata non avrebbe più senso; Comunque scopriremo che la radice quadrata è un operazione elementare: ciò vuol dire che il tempo impiegato per effettuare SQRT(1) è uguale al tempo impiegato per effettuare SQRT(100000000000000..0)!

Il codice PHP Parallelizzato

Come trasformare tutto ciò in PHP?
(la procedura prevede la conoscenza di un po’ di javascript e di html)

Impostiamo i seguenti file:

thread1.php

<?php

function microtime_float() //funzione che mi serve per calcolare con precisione i tempi

{

list($usec, $sec)=explode(" ", microtime());

return ((float)$usec + (float) $sec);

}

$inizio=microtime_float(); //salvo in inizio l'ora di avvio dello script

set_time_limit(300000000000); //setto un tempo molto lungo per l'esecuzione

$somma1=0; //elemento neutro per la somma

for ($i=1; $i <= 99999999; $i+=2) / $i varia da 1 a 99999999 a passi di 2

{

	$somma1+=sqrt($i); //aggiungo alla somma il risultato della radice quadrata

}

$handle=fopen("processore1.result","w+"); //salvo il risultato della sommatoria parziale

fwrite($handle,$somma1);

fclose($handle);

$fine=microtime_float(); //salvo l'ora di fine dello script

$tempo_impiegato=$fine - $inizio; //mi calcolo il tempo impiegato

$tempo=number_format($tempo_impiegato,5,',','.'); //trasformo il tempo impiegato in secondi

$handle=fopen("processore1.time","w+"); //salvo il tempo impiegato

fwrite($handle,$tempo);

fclose($handle);

?>

thread2.php

<?php

//i commenti sono simili al precedente

function microtime_float()

{

list($usec, $sec)=explode(" ", microtime());

return ((float)$usec + (float) $sec);

}

$inizio=microtime_float();

set_time_limit(300000000000);

$somma2=0; //elemento neutro per la somma

for ($i=2; $i <= 100000000; $i+=2)

{

	$somma2+=sqrt($i);

}

$handle=fopen("processore2.result","w+");

fwrite($handle,$somma2);

fclose($handle);

$fine=microtime_float();

$tempo_impiegato=$fine - $inizio;

$tempo=number_format($tempo_impiegato,5,',','.');

$handle=fopen("processore2.time","w+");

fwrite($handle,$tempo);

fclose($handle);

?>

parallelizza.htm

<html>

<head>

<script type="text/javascript">

processore1=false;

processore2=false;

function finito(processore)

{

if (processore==1) //se thread1.php ha finito imposto processore1 a true

processore1=true;

if (processore==2)//se thread2.php ha finito imposto processore2 a true

processore2=true;

if ((processore1==true) && (processore2==true))

document.location.href="risultato.php";

//se i thread hanno finito entrambi mi rediriggo alla pagina risultato.php

}

</script>

</head>

<body>

<iframe onload="finito(1)" src="thread1.php">

<iframe onload="finito(2)" src="thread2.php" id="ProcessoreDue">

/* quando un frame finisce di caricare (onload) vuol dire che un

/* thread ha finito e chiama la funzione precedentemente

/* commentata in javascript

</body>

</html>

risultato.htm

<?php

$handle=fopen("processore1.result","r");

$risultato1=fread($handle,8000);

fclose($handle);

//metto in risultato1 la somma parziale di thread1.php

$handle=fopen("processore2.result","r");

$risultato2=fread($handle,8000);

fclose($handle);

//metto in risultato2 la somma parziale di thread2.php

echo "Somma: ";

echo $risultato1+$risultato2;

echo "
";

//visualizzo la somma

$handle=fopen("processore1.time","r");

$time1=fread($handle,8000);

fclose($handle);

//prendo il tempo di thread1.php

$handle=fopen("processore2.time","r");

$time2=fread($handle,8000);

fclose($handle);

//prendo il tempo di thread2.php

if ($time1>$time2)

echo "Tempo impiegato: $time1 secondi";

else

echo "Tempo impiegato: $time2 secondi";

//visualizzo il tempo maggiore tra thread1.php e thread2.php come tempo totale

?>

Per quanto il codice dovrebbe essere di semplice interpretazione, passiamo a qualche spiegazione:

I file thread1.php e thread2.php si dividono il lavoro, effettuano metà lavoro ciascuno (utilizzando due processori)

Il file parallelizza.htm serve per avviare contemporaneamente i due file thread1.php e thread2.php in un iframe, modo che siamo sicuri che i due thread partono contemporaneamente e per avviare il file risultato.php quando entrambi i due thread hanno finito il lavoro (parte in javascript).

Il file risultato.php che si avvia soltanto quando tutti e due i thread hanno finito di effettuare il lavoro somma i due risultati e visualizza i risultato finale.

Codice non parallelizzato

Senza programmazione parallela lo script php sarebbe:

singolo.php

<?php

function microtime_float()

{

list($usec, $sec)=explode(" ", microtime());

return ((float)$usec + (float) $sec);

}

$inizio=microtime_float(); //calcolo l'ora di inizio

set_time_limit(300000000000);

$somma2=0; //elemento neutro per la somma

for ($i=1; $i <= 100000000; $i++)

{

	$somma2+=sqrt($i); //effettuo il ciclo a passi di 1

}

echo "Somma: $somma2
"; //visualizzo la somma

$fine=microtime_float(); //calcolo l'ora di fine

$tempo_impiegato=$fine - $inizio;

$tempo=number_format($tempo_impiegato,5,',','.');

echo "Tempo impiegato: $tempo secondi"; //visualizzo il tempo impiegato

?>

Prestazioni a confronto

Analizziamo adesso le differenze prestazionali dei due software e come utilizzano il processore.

Il processore del computer in questione è un Intel Core 2 Duo.

Per il software non parallelizzato abbiamo i seguenti risultati:

Per il software parallelizzato abbiamo i seguenti risultati:

Si evince come il software parallelizzato utilizzi al 100% la capacità di calcolo del processore dimezzando praticamente i tempi rispetto al software non parallelizzato.

In PC monoprocessore i tempi sono praticamente uguali tra software parallelizzato e non.

SQRT, operazione elementare

Verifichiamo adesso che l’operazione di SQRT sia un’operazione elementare (ne parlavamo qualche pagina prima).

Sostituiamo nei file che compongono il software parallelo i file thread1.php e thread2.php con questi file:

thread1.php

<?php

function microtime_float()

{

list($usec, $sec)=explode(" ", microtime());

return ((float)$usec + (float) $sec);

}

$inizio=microtime_float();

	set_time_limit(300000000000);

	$somma1=0; //elemento neutro per la somma

	for ($i=1; $i <= 50000000; $i++) //il for va da 1 a 50000000

	{

		$somma1+=sqrt($i);

	}

$handle=fopen("processore1.result","w+");

fwrite($handle,$somma1);

fclose($handle);

$fine=microtime_float();

$tempo_impiegato=$fine - $inizio;

$tempo=number_format($tempo_impiegato,5,',','.');

$handle=fopen("processore1.time","w+");

fwrite($handle,$tempo);

fclose($handle);

?>

thread2.php

<?php

function microtime_float()

{

list($usec, $sec)=explode(" ", microtime());

return ((float)$usec + (float) $sec);

}

$inizio=microtime_float();

	set_time_limit(300000000000);

	$somma2=0; //elemento neutro per la somma

	for ($i=50000001; $i <= 100000000; $i++) //il for va da 50000000 a 100000000

	{

		$somma2+=sqrt($i);

	}

$handle=fopen("processore2.result","w+");

fwrite($handle,$somma2);

fclose($handle);

$fine=microtime_float();

$tempo_impiegato=$fine - $inizio;

$tempo=number_format($tempo_impiegato,5,',','.');

$handle=fopen("processore2.time","w+");

fwrite($handle,$tempo);

fclose($handle);

?>

Si potrebbe pensare che thread1.php termini molto prima rispetto thread2.php in quanto fare le radici che vanno da 1 a 50.000.000 (thread1.php) sia più facile che farle da 50.000.000 a 100.000.000; invece questo risulta falso in quando i processori di oggi sono così ottimizzati da effettuare operazioni come la radice quadrata con un singolo ciclo di clock (l’affermazione potrebbe non corrispondere esattamente al vero però in termini realistici va più che bene).
Vediamo i risultati:

Praticamente non vi è alcuna differenza tra i due metodi di parallelizzazione.

Note finali

Il risultato finale dei diversi applicativi paralleli e di singolo.php variano di qualche decimale per errori dovuti all’approssimazione della somma effettuata successivamente all’esecuzione dei thread.

Il più preciso risulta quindi singolo.php: ciò non toglie il fatto che aumentando la precisione dei decimali il risultato degli applicativi paralleli possa coincidere a quello di singolo.php

Download

Nome Dimensione Descrizione
parallela.zip 13 Kbytes Esempio di programmazione parallela in PHP (richiesta conoscenza di javascript e html)