W pracy, udało nam się przepisać celery z Pythona na nasz własny menedżer tasków w Rust.
Wydajność jest zauważalnie lepsza, bo korzystamy ze starych komponentów i tam różnice są o wiele bardziej widoczne, niż na zwykłym komputerze, gdzie oba rozwiązania przetwarzają zadania niemal natychmiast.

Zatem mamy część z kodem w Rust, ale ciągle część z obliczeń musimy wykonać w pojedynczym procesie pythona do którego wysyłamy resty z zapytaniami.
Jednak celery miało jeden duży plus - uruchamiało się jako osobne procesy(co wydłużało o kilka sekund proces uruchamiania programu), dzięki czemu operacje numpy/scipy na CPU, były ładnie rozdzielane po stronie pythona na wszystkie wątki(na urządzeniu są 4 rdzenie i tyle samo procesów celery było uruchamianych)

Operacje obliczania statystyk(mowa o powyższym zadaniu korzystającym z numpy/scipy) są często zlecane w większej ilości na raz, dlatego ważne jest by to zrównoleglić.
Próbowałem robić to przez proste rozdzielanie tasków na wiele wątków, jednak, czasy bywały nawet czasami gorsze, niż jak robiłem to w jednym wątku - domyślam się że to wina GIL i tego że nie działa zbytnio dobrze, gdy wątki wykonują masę rzeczy na cpu.
Drugim problemem jest to że w przypadku równoległych działań wyskakują dziwne ostrzeżenia, przez co myślę że np. matplotlib, nie jest przystosowany do działania z wielu wątków(mimo że zadania które wykonuję od początku do końca działają tylko w jednym wątku), bo przechowuje globalnie jakieś parametry ze swoim stanem.

Kojarzycie, w jaki sposób, mógłbym w miarę prosto, móc wykonywać te obliczenia po stronie pythona na wielu wątkach?
#python
markxvyarov

@qarmin jest standardowy moduł w Pythonie, nazywa się multiprocessing. W nim znajdziesz Pool i metody typu starmap i map. Najczęściej z tego korzystam gdy trzeba zrównoległość obliczenia.


https://superfastpython.com/multiprocessing-pool-map/

# SuperFastPython.com

# example of parallel map() with the process pool

from random import random

from time import sleep

from multiprocessing.pool import Pool

# task executed in a worker process

def task(identifier):

    # generate a value

    value = random()

    # report a message

    print(f'Task {identifier} executing with {value}', flush=True)

    # block for a moment

    sleep(value)

    # return the generated value

    return value

# protect the entry point

if __name__ == '__main__':

    # create and configure the process pool

    with Pool() as pool:

        # execute tasks in order

        for result in pool.map(task, range(10)):

            print(f'Got result: {result}', flush=True)

    # process pool is closed automatically


Na zamieszczonym zdjęciu znajdziesz wszystkie metody z klasy Pool które możesz wykorzystać.

eaab2cd3-50c4-480c-aa4f-b776373b7c0f
qarmin

@markxvyarov Działa to lepiej, niż ręczne tworzenie i zarządzanie wątkami?

Boję się że również tutaj GIL pokaże swoje ograniczenia i w rzeczywistości będzie to wszystko działało, niemal tak jak w jednym wątku

markxvyarov

@qarmin dla tego podałem multiprocessing a nie threading. Przy tym drugim, wątki są blokowane przez GIL, i są one obsługiwany tylko przez jeden rdzeń procesora. Najczęściej wykorzystuje się do zrównoleglenia operacji IO. Tym czasem multiprocessing uruchamia oddzielne procesy pochodne od głównego, dlatego każdy proces może być w tym samym czasie obsłużony przez CPU.


Metoda którą Ci podałem jest o tyle prosta że o nic nie trzeba dbać, a funkcje zwracają wyniki obliczeń.

Zaloguj się aby komentować