Programació en paral.lel
Un programa Python només utilitza 1 CPU del processador encara que el processador tingui vàries CPUs disponbibles.
Amb el mòdul multiprocessing podem executar una part del nostre programa en un nou procés que s’executarà en una altre CPU del processador.
Això es fa quan tens una funció computacionalment intensiva, que vol dir que ha de fer moltes coses i tarda molt en fer-les com es calcular el Factorial d’un número bastant gran.
A continuació tens una funció factorial:
=
= 1
= *
Si executes dos cops la funció factorial() pots veure que el programa tarda més d’1 segon en executar-se perquè fins que no s’ha acabat d’executar la primera crida a la funció factorial()no és pot executar la segona crida a la funció factorial().
=
= 1
= *
Si executes el programa pots veure que el progama tarda uns 5 segons en executar-se perquè només utiliza un procés i una CPU:
Amb la llibreria multiprocessing podem crear un procés per executar la tasca factorial.
...
= Quan cridem la funció start() de l’objecte Process el sistema operatiu crearà un nou procés per executar la funció corresponent.
=
= 1
= *
Pots veure que el programa s’executa en poc més d’3 segons perquè les tasques s’executen en paral.lel en procesos diferents en CPUs diferents:
El temps real és el temps que ha tardat en executar-se el programa mentres que user i sys és temps d’execució total en CPUs.
Si et fixes en la sortida, les tasques s’executen més tard que el print('Program finished').
Amb la funció join() podem indicar que el procés que està executant el programa ha d’esperar a que acabi el nou procés que ha creat abans de seguir executant més codi i imprimir per pantalla Program finished.
Per exemple, podem fer un join() amb el segon procés:
=
= 1
= *
=
=
Pots veure que el procés principal només espera el procés p2 per seguir executant codi:
Activitat. Modifica el codi perquè el procés principal esperi que acabi el procés p1 enlloc del procés p2.
CPUs bound
L’execució de procesos en paral.lel està limitat pel les CPUs que té el processador.
En el meu cas el meu processador és un Intel(R) Xeon(R) Platinum 8358 CPU @ 2.60GHz amb 8 CPUs tal com es pot veure a continuació:
Tu has d’adaptar els exemples al teu processador.
Anem a provar que passa executan 6 procesos al mateix temps:
=
= 1
= *
=
Pots veure que el programa s’executa en 3,216 segons, però el temps d’execució de CPUs és de 16,445 segons en espai d’usuari:
En canvi si executem més procesos que CPUs té el procesador, no tots els procesos es poden executar a la vegada i el temps real será superior als 3 segons perquè els procesos es van executant de manera cooperativa compartint les CPUs.
Si executem 16 processos el temps real d’execució és de 5,621 segons, i tots els processos tarden entre 4 i 6 segons en executar-se encara que es poden executar en 3 segons si tenen una CPU en exclusiva només per a ells:
Pool manager
Tenir molts procesos en execució al mateix temps no és molt bona idea.
Imagina`t una classe de 80 alumnes amb només 4 ordinadors tots junts a la mateixa aula compartint els ordinadors per fer una tasca.
És millor que només hi hagi 4 alumnes treballant i a mida que un acabi una altre alumne ocupi l’ordinador.
Ja ser que hi haurà discusions de qui va primer, que aquest alumne tarda massa, etc. , i el professor ha de decidir de la mateixa manera que tu com a programador tens que decidir quins processos tenen prioritat, etc.
Per limitar el número de processos que estan en execució a la vegada pots utilitzar un Pool:
=
=
Per defecte mp.Pool() crea un pool amb el número de CPUS que té el processador, encara que pots passar per argument un número diferent.
Amb la funció apply_async passem la funció que s’ha d’executar en el nou procés.
Com que volem es esperar a que el procés acabi d’executarse fem servir la funció get().
Pots verificar que tots els processos s’executen en uns 3 segons.
Si vols escriure menys codi pots utilitzar la funció map():
=
We don’t have the start and join here because it is hidden behind the pool.map() function. What it does is split the iterable range(1,1000) into chunks and runs each chunk in the pool.
Amb la funció map no tenim que fer un start i un join perqè la funció map ja s’ocupa de fer-ho.
concurrent.futures
Enlloc de fer servir el mòdul multiprocessing podem utilitzar el mòdul concurrent.features:
This code is running the multiprocessing module under the hood. The beauty of doing so is that we can change the program from multiprocessing to multithreading by simply replacing ProcessPoolExecutor with ThreadPoolExecutor. Of course, you have to consider whether the global interpreter lock is an issue for your code.
La funció ProcessPoolExecutor utilitza el mòdul multiprocessing.
L’avantatge més important és que la lógica del codi és la mateixa en multiprocessament que en multithreading: només has de substituir ProcessPoolExecutor per ThreadPoolExecutor.
joblib
The package joblib is a set of tools to make parallel computing easier. It is a common third-party library for multiprocessing. It also provides caching and serialization functions. To install the joblib package, use the command in the terminal: pip install joblib
We can convert our previous example into the following to use joblib: import time from joblib import Parallel, delayed
def cube(x): return x**3
start_time = time.perf_counter() result = Parallel(n_jobs=3)(delayed(cube)(i) for i in range(1,1000)) finish_time = time.perf_counter() print(f“Program finished in {finish_time-start_time} seconds“) print(result)
Indeed, it is intuitive to see what it does. The delayed() function is a wrapper to another function to make a “delayed” version of the function call. Which means it will not execute the function immediately when it is called.
Then we call the delayed function multiple times with different sets of arguments we want to pass to it. For example, when we give integer 1 to the delayed version of the function cube, instead of computing the result, we produce a tuple, (cube, (1,), {}) for the function object, the positional arguments, and keyword arguments, respectively.
We created the engine instance with Parallel(). When it is invoked like a function with the list of tuples as an argument, it will actually execute the job as specified by each tuple in parallel and collect the result as a list after all jobs are finished. Here we created the Parallel() instance with n_jobs=3, so there will be three processes running in parallel.
We can also write the tuples directly. Hence the code above can be rewritten as: result = Parallel(n_jobs=3)((cube, (i,), {}) for i in range(1,1000))
The benefit of using joblib is that we can run the code in multithread by simply adding an additional argument: result = Parallel(n_jobs=3, prefer=“threads”)(delayed(cube)(i) for i in range(1,1000))
And this hides all the details of running functions in parallel. We simply use a syntax not too much different from a plain list comprehension.