Multiples interprets de python embebits en un multi thread environment

September 27, 2007 – 2:12 am

L'execució de múltiples intèrprets - també dits subinterprets - en una aplicació multi thread es habitual en certs projectes com ara mod_python.

Mitjançant la creació de múltiples intèrprets podem crear entorns d'execució de codi python totalment transparent entre fils. Altrament dit aïllats.

Quant diem aïllats, parlem d'un sys.modules propis, d'uns sys.stdou, sys.stdin i sys.stderr propis i altres propietats que l'haurien de fer veure com un procés independent.

No usar múltiples intèrprets ens pot portar a que tots els threads comparteixin un mateix conjunt de propietats i estat de l'interpret. Un exemple clar d'on no ens agradaria tenir aquesta situació és la següent.

Tenim un thread de python que executa algunes comandes de python, entre les quals tenim un redireccionament del sys.stdout a una pipe, a partir d'aquell moment tots els altres threads que comparteixin aquell interpret també herederaran aquesta situació.

Reflexionant una mica sobre les propietats dels subinterprets i la utilització de mod_python que en fa, puc quasi assegurar el següent.

1. Si heu provat de modificar el codi d'un modul exportat per el handler principal del mod_python haureu vist que aquest no es realment re-importat amb el codi nou fins que no reiniciem el apache, aquesta es una propietat de la reutilizació de intèrprets associats als mateixos virtual servers que fa l'apache i el mod_python

2. Un Virtual Server només pot correr en un mateix moment un interprete, per tant només pot atendre una petició al mateix temps, aquesta afirmació pot ser molt dolorosa i estic pendent de poder-la testejar.

Be tot i les dramàtiques conseqüències de reutilizar un interpret podem assegurar que aquest tenen una funcionalitat i objectiu força clars, aïllar diferents execucions de processo python amb "consoles" o diguis intèrprets que no mantinguin relacions.

Per a poder aclarir els conceptes anteriors veurem un conjunt de pedaços de codi de la rutina d'un thread anomenada do_something_python_code que anirem evolucionant fins a una tercera versió final on inclourem tots els mecanismes adequats per a un bon funcionament de la rutina i amb totes les capacitats avançades que ens brinda python.

Pero primerament seria bo veure el main que comparteixen, aquest en definitiva es dedica a aixecar n subinterprets perquè aquest siguin executats de forma paral.lela per n threads

int main( int arc, char * argv[] )
{
int i, status;

// initialize python
Py_Initialize();

// initialize thread python support
PyEval_InitThreads();

// release GIL
PyEval_ReleaseLock();

// get thread state of main interpreter
global_state = PyThreadState_Get();

// Create n subinterpreters
printf("Creating sub interpreters n");
for ( i = 0; i < N_CONCURRENT; i++)
{
interpreter[i] = Py_NewInterpreter();
PyThreadState_Swap(global_state);
}

// Create n threads
printf("Creating threads n");
for ( i = 0; i < N_CONCURRENT; i++)
pthread_create(&id_thread[i], NULL, do_something_python_code, (void *)i);

// wait for finalitze all threads
printf("Waiting threads n");
for ( i = 0; i < N_CONCURRENT; i++)
pthread_join(id_thread[i], (void **) &status);

Py_Finalize();
return 0;
}

Comentar del codi anterior les seguents parts. Mitjançant la funció Py_NewInterpreter() creem un nou interpret i un nou thread de control d'aquest interpret, cada cop que fem aixo la funció en questió fa un swap a aquest nou context thread. Per tant cal tornar a posicionar el context al thread principal mitjançant la línia PyThreadState_Swap(global_state);

Cal tenir en compte que python no te un concepte pur de fil o thread, te un concepte de context de fil o thread, i que som nosaltres que assignarem un context - amb tota la informació associada a aquest com ara el interpret associat - en un thread determinat

Del codi anterior tambe destecar la creació dels trheads mitjançant les crides reiterades a la funció pthread_create, posteriorment farem els joins corresponents esperant la finalització de tots ells

En una primera versió de la funció do_something_ptyhon_code, vaig simplificar la seva forma tal com esta presentada

void * do_something_python_code (void * id)
{
char command[512];
int id_cast = (int ) id;

printf("Thread %d execution n", id_cast);

// adquire my sane interpreter
PyThreadState_Swap(interpreter[id_cast]);

// do something
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);
PyRun_SimpleString("import sysn");
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);

// finalize my interpreter
Py_EndInterpreter(interpreter[id_cast]);

// return to main interpreter
PyThreadState_Swap(global_state);

printf("Thread %d ending n", id_cast);

pthread_exit((void *) 0);
}

La sortida del programa anterior donava una cosa semblant :

pfreixes@hidrogen:~/temp/test_python$ ./test
Creating sub interpreters
Creating threads
Waiting threads
Thread 0 execution
(0 ['__builtins__', '__doc__', '__name__'])
(0 ['__builtins__', '__doc__', '__name__', 'sys'])
Thread 0 ending
Thread 1 execution
(1 ['__builtins__', '__doc__', '__name__'])
(1 ['__builtins__', '__doc__', '__name__', 'sys'])
Thread 1 ending
Thread 2 execution
(2 ['__builtins__', '__doc__', '__name__'])
(2 ['__builtins__', '__doc__', '__name__', 'sys'])
Thread 2 ending
Thread 3 execution
(3 ['__builtins__', '__doc__', '__name__'])
(3 ['__builtins__', '__doc__', '__name__', 'sys'])

Com podeu comprovar el codi anterior semblava que era correcta, a cada interpret s'executaven tres linies de python, una primera demanant amb un dir() quins moduls tenia important, una segona important el modul sys i finalment una tercera tornant a demanar els moduls importats per poder saber si l'anterior crida havia efectuat el import del modul sys.

Pero bé, abans de veure alguns errors en el codi anterior, voldria fer un repas de la primera versió de la funció do_something_python_code

Cal destecar primer la crida a la funció PyThreadState_Swap(interpreter[id_cast]), mitjançant aquesta el que fem es dir a python que el thread actual tindrà el contexte de l'interpret interpreter[id_cast], i que per tant totes les operacions sobre codi python - sigui simples crides de literals com les vistes o sobre objectes exposats a C des de python - es faran sobre l'entorn d'aquest interpret, aixo es potser el més important de tot l'exercici.

Tambe destacar que per poder retornar el control al context del thread o interpret principal sempre cal fer un PyThreadState_Swap(global_state);

Pero com haurem pogut veure no hem vist que els diferents threads hagin alternat les seves sortides de pantalla, i que primer ho ha fet el interpret 0, per fer-ho seguidament el 1, el 2 i el 3 respectivament.

Això és degut basicament al temps dedicat a executar codi no ha sigut el suficientment gran per produir-se un canvi de context - hem permeteu aquesta generalització process/thread oi. Per poder modificar aixo i cerca l'encabalcament de threads vaig modificar el codi intermig de la rutina per el seguent

// do something
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);
PyRun_SimpleString("import sysn");
// for testing concurrency only
sleep(2);
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);
............

Si provem d'executar el codi seguent podem veure que es produeix un error in runtime de python, perque ? bet and win !! si exactament si nosaltres fem un canvi de context en el moment que el thread 1 fa un sleep, seguidament el thread 2 agafa la cpu i posiciona el seu context amb la crida PyThreadState_Swap(interpreter[id_cast]) si despres el thread 1 torna a agafar la cpu sobre quin context s'estara executant, sobre el del segon thread i/o segon interpret i aixo es totalment incorrecte. Recordem que estem treballant amb una relació 1:1 entre interprets i threads, per tan cal respectar en tot moment que el thread que assumit un context i executi n linies de codi python l'alliberi ell i no que algú altre - en aquest cas un altre thread - el modifiqui.

Per evitar aquesta situació, cal bloquejar el que anomenem GIL ( Global Interpreter Lock ), mitjançant el bloqueig d'aquest evitem que un altre thread ens prengui la CPU en el mooment que estem executant codi python d'un interpret

Per tan la funció podria quedar alguna cosa semblant a aixo:

void * do_something_python_code (void * id)
{
char command[512];
int id_cast = (int ) id;

printf("Thread %d execution n", id_cast);

// adquire my sane interpreter
PyEval_AcquireLock();
PyThreadState_Swap(interpreter[id_cast]);

// do something
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);
PyRun_SimpleString("import sysn");

// for testing concurrency only
sleep(2);

sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);

// finalize my interpreter
Py_EndInterpreter(interpreter[id_cast]);

// return to main interpreter
PyThreadState_Swap(global_state);
PyEval_ReleaseLock();

printf("Thread %d ending n", id_cast);

pthread_exit((void *) 0);
}

Si executem el codi amb les modificacions anteriors veurem que ara ja no es produeixen errors, pero tindrem de forma contraproduent un altre problema. No hi haura concurrencia. El GIL bloquejara l'acces als altres threads i per tant només un thread en cada moment podra executar la funció anterior.

Pero com comenta les C API extensions de Python podem evitar aquest tipus de problemes mitjançant la cessio momentania de la CPU en certs moments de bloqueig, i clar un sleep de 2 segons se suposa que es un moment de bloqueig.

Per a poder fer aixo, modificarem la funcio anterior per aquesta altre funció

void * do_something_python_code (void * id)
{
char command[512];
int id_cast = (int ) id;

printf("Thread %d execution n", id_cast);

// adquire my sane interpreter
PyEval_AcquireLock();
PyThreadState_Swap(interpreter[id_cast]);

// do something
sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);
PyRun_SimpleString("import sysn");

// for testing concurrency only

Py_BEGIN_ALLOW_THREADS
sleep(2);
Py_END_ALLOW_THREADS

sprintf(command, "print \"(%d %cs)\" %c dir()n�", id_cast, '%', '%');
PyRun_SimpleString(command);

// finalize my interpreter
Py_EndInterpreter(interpreter[id_cast]);

// return to main interpreter
PyThreadState_Swap(global_state);
PyEval_ReleaseLock();

printf("Thread %d ending n", id_cast);

pthread_exit((void *) 0);
}

Mitjançant la macro Py_BEGIN_ALLOW_THREADS i el seu oposat podem conseguir la concurrencia desitjada.

Treballar amb threads té les seves peculiaritats, i fer.ho en un entorn com el presentat pot esdevenir certament complex

  1. 1 Trackback(s)

  2. Sep 27, 2007: elMeu blog » Afinant l’entorn multi thread i multiples interprets de python

Post a Comment

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word