Greenlet Vs. Нитки


141

Я новачок у гевен та зелених грінках. Я знайшов гарну документацію про те, як працювати з ними, але жодна не дала мені обґрунтування того, як і коли я повинен використовувати зелені!

  • У чому вони насправді гарні?
  • Корисно використовувати їх на проксі-сервері чи ні?
  • Чому б не нитки?

Я не впевнений у тому, як вони можуть забезпечити нам паралельність, якщо вони в основному є спільними процедурами.


1
@Imran Йдеться про грецькі читання на Java. Моє запитання - про зелений в Python. Я щось пропускаю?
Rsh

Афаік, потоки в python насправді не є спільними з-за глобального блокування інтерпретатора. Таким чином, це зводиться до порівняння накладних витрат обох рішень. Хоча я розумію, що існує кілька реалізацій python, тому це може стосуватися не всіх.
didierc

3
@didierc CPython (і PyPy відтепер) не буде інтерпретувати паралельно Python (байт) код (тобто реально фізично одночасно на двох різних ядрах процесора). Однак, не все, що робить програма Python, знаходиться під GIL (поширені приклади - це систематичні виклики, включаючи функції вводу / виводу та функції C, які свідомо вивільняють GIL), а a threading.Thread- насправді OS-потік з усіма наслідками. Так що це насправді не так просто. До речі, у Jython немає GIL AFAIK і PyPy намагається позбутися і цього.

Відповіді:


204

Зелені рослини забезпечують одночасність, але не паралелізм. Паралельність - це коли код може працювати незалежно від іншого коду. Паралелізм - це виконання одночасного коду одночасно. Паралелізм особливо корисний, коли потрібно зробити багато роботи в просторі користувачів, і це, як правило, важкі для процесора речі. Паралельність корисна для розв’язання проблем, що дозволяє розпланувати різні частини та керувати ними легше паралельно.

Greenlets дійсно сяють в мережевому програмуванні, де взаємодія з одним сокетом може відбуватися незалежно від взаємодії з іншими сокетами. Це класичний приклад одночасності. Оскільки кожен greenlet працює у власному контексті, ви можете продовжувати використовувати синхронні API без нарізування різьби. Це добре, тому що потоки є дуже дорогими з точки зору віртуальної пам’яті та накладних витрат ядра, тому сумісність, яку ви можете досягти з потоками, значно менша. Крім того, через GIL нитки в Python дорожчі та обмеженіші, ніж зазвичай. Альтернативою паралельності зазвичай є проекти, такі як Twisted, libevent, libuv, node.js тощо, де весь ваш код має однаковий контекст виконання та реєструє обробників подій.

Відмінна ідея використовувати greenlets (з відповідною мережевою підтримкою, наприклад, через gevent) для написання проксі-сервера, оскільки обробка запитів може виконуватись самостійно і повинна бути записана як така.

Зелені рослини забезпечують паралельність з тих причин, які я викладав раніше. Паралельність - це не паралелізм. Приховуючи реєстрацію подій та виконуючи для вас планування дзвінків, які зазвичай блокують поточну нитку, такі проекти, як gevent, розкривають цю сумісність, не вимагаючи змін до асинхронного API та значно менших витрат для вашої системи.


1
Дякую, лише два невеликі запитання: 1) Чи можливо комбінувати це рішення з багатопроцесорним процесом для досягнення більшої пропускної здатності? 2) Я досі не знаю, навіщо використовувати нитки? Чи можемо ми розглядати їх як наївну та базову реалізацію паралельності в стандартній бібліотеці пітона?
Rsh

6
1) Так, абсолютно. Не слід робити це передчасно, але через цілу купу факторів, що виходять за рамки цього питання, наявність декількох запитів на обслуговування процесів дасть вам більшу пропускну спроможність. 2) потоки ОС попередньо заплановані та повністю паралельні за замовчуванням. Вони за замовчуванням у Python, оскільки Python розкриває власний інтерфейс для потоків, а потоки є найкращим підтримуваним і найнижчим загальним знаменником як для паралелізму, так і для одночасності в сучасних операційних системах.
Метт Столяр

6
Я мушу зазначити, що ви навіть не повинні використовувати greenlets до тих пір, поки потоки не будуть задовільними (зазвичай це відбувається через кількість одночасних з'єднань, якими ви обробляєтесь, і або кількість потоків, або GIL доставляють вам горе), і навіть то тільки тоді, якщо вам не доступна інша опція. Стандартна бібліотека Python та більшість сторонніх бібліотек очікують, що одночасність буде досягнута за допомогою потоків, тож ви можете отримати дивну поведінку, якщо надасте це через greenlets.
Метт Столяр

@MattJoiner У мене є нижченаведена функція, яка читає величезний файл для обчислення суми md5. як я можу в цьому випадку використовувати gevent для швидшого читання import hashlib def checksum_md5(filename): md5 = hashlib.md5() with open(filename,'rb') as f: for chunk in iter(lambda: f.read(8192), b''): md5.update(chunk) return md5.digest()
Soumya

18

Отримавши відповідь @ Макса і додавши до неї певну актуальність для масштабування, ви можете побачити різницю. Я домігся цього, змінивши URL-адреси, які потрібно заповнити так:

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
URLS = []
for _ in range(10000):
    for url in URLS_base:
        URLS.append(url)

Мені довелося відмовитися від багатопроцесорної версії, як вона випала до того, як у мене вийшло 500; але на 10000 ітерацій:

Using gevent it took: 3.756914
-----------
Using multi-threading it took: 15.797028

Таким чином, ви можете бачити, що існує якась суттєва різниця вводу / виводу за допомогою gevent


4
абсолютно невірно створювати 60000 натільних потоків або процесів для завершення роботи, і цей тест нічого не показує (також ви зняли тайм-аут виклику gevent.joinall ()?). Спробуйте використовувати пул потоків близько 50 ниток, див моєї відповіді: stackoverflow.com/a/51932442/34549
zzzeek

9

Виправляючи відповідь @TemporalBeing вище, зелені зелені не є "швидшими", ніж потоки, і це неправильна техніка програмування, щоб породити 60000 ниток для вирішення проблеми одночасності, натомість невеликий пул потоків. Ось більш розумне порівняння (з моєї публікації reddit у відповідь на людей, які цитують цю публікацію SO).

import gevent
from gevent import socket as gsock
import socket as sock
import threading
from datetime import datetime


def timeit(fn, URLS):
    t1 = datetime.now()
    fn()
    t2 = datetime.now()
    print(
        "%s / %d hostnames, %s seconds" % (
            fn.__name__,
            len(URLS),
            (t2 - t1).total_seconds()
        )
    )


def run_gevent_without_a_timeout():
    ip_numbers = []

    def greenlet(domain_name):
        ip_numbers.append(gsock.gethostbyname(domain_name))

    jobs = [gevent.spawn(greenlet, domain_name) for domain_name in URLS]
    gevent.joinall(jobs)
    assert len(ip_numbers) == len(URLS)


def run_threads_correctly():
    ip_numbers = []

    def process():
        while queue:
            try:
                domain_name = queue.pop()
            except IndexError:
                pass
            else:
                ip_numbers.append(sock.gethostbyname(domain_name))

    threads = [threading.Thread(target=process) for i in range(50)]

    queue = list(URLS)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    assert len(ip_numbers) == len(URLS)

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org',
             'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']

for NUM in (5, 50, 500, 5000, 10000):
    URLS = []

    for _ in range(NUM):
        for url in URLS_base:
            URLS.append(url)

    print("--------------------")
    timeit(run_gevent_without_a_timeout, URLS)
    timeit(run_threads_correctly, URLS)

Ось деякі результати:

--------------------
run_gevent_without_a_timeout / 30 hostnames, 0.044888 seconds
run_threads_correctly / 30 hostnames, 0.019389 seconds
--------------------
run_gevent_without_a_timeout / 300 hostnames, 0.186045 seconds
run_threads_correctly / 300 hostnames, 0.153808 seconds
--------------------
run_gevent_without_a_timeout / 3000 hostnames, 1.834089 seconds
run_threads_correctly / 3000 hostnames, 1.569523 seconds
--------------------
run_gevent_without_a_timeout / 30000 hostnames, 19.030259 seconds
run_threads_correctly / 30000 hostnames, 15.163603 seconds
--------------------
run_gevent_without_a_timeout / 60000 hostnames, 35.770358 seconds
run_threads_correctly / 60000 hostnames, 29.864083 seconds

непорозуміння, яке має кожен про незаблокування вводу-виводу з Python, - це переконання, що інтерпретатор Python може брати участь у роботі з пошуку результатів з сокетів у більшій мірі швидше, ніж самі мережеві з'єднання можуть повернути IO. Хоча це, безумовно, вірно в деяких випадках, це неправда майже так часто, як думають люди, тому що інтерпретатор Python дійсно, дуже повільний. У своїй публікації в блозі я проілюструю деякі графічні профілі, які показують, що навіть для дуже простих речей, якщо ви маєте справу з чітким та швидким доступом до мережі до таких речей, як бази даних або сервери DNS, ці служби можуть повернутися набагато швидше, ніж код Python може відвідувати багато тисяч цих зв’язків.


8

Це досить цікаво для аналізу. Ось код для порівняння продуктивності парникових та багатопроцесорних пулів проти багатопотокових:

import gevent
from gevent import socket as gsock
import socket as sock
from multiprocessing import Pool
from threading import Thread
from datetime import datetime

class IpGetter(Thread):
    def __init__(self, domain):
        Thread.__init__(self)
        self.domain = domain
    def run(self):
        self.ip = sock.gethostbyname(self.domain)

if __name__ == "__main__":
    URLS = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
    t1 = datetime.now()
    jobs = [gevent.spawn(gsock.gethostbyname, url) for url in URLS]
    gevent.joinall(jobs, timeout=2)
    t2 = datetime.now()
    print "Using gevent it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    pool = Pool(len(URLS))
    results = pool.map(sock.gethostbyname, URLS)
    t2 = datetime.now()
    pool.close()
    print "Using multiprocessing it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    threads = []
    for url in URLS:
        t = IpGetter(url)
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    t2 = datetime.now()
    print "Using multi-threading it took: %s" % (t2-t1).total_seconds()

ось результати:

Using gevent it took: 0.083758
-----------
Using multiprocessing it took: 0.023633
-----------
Using multi-threading it took: 0.008327

Я думаю, що greenlet стверджує, що він не пов'язаний GIL на відміну від багатопотокової бібліотеки. Більше того, доктор Greenlet каже, що він призначений для роботи в мережі. Для мережевої інтенсивної роботи перемикання потоків добре, і ви можете бачити, що багатопотоковий підхід досить швидкий. Також завжди бажано використовувати офіційні бібліотеки python; Я спробував встановити greenlet на windows і зіткнувся з проблемою залежності dll, тому я запустив цей тест на Linux vm. Завжди намагайтеся написати код з надією, що він працює на будь-якій машині.


25
Зауважте, що getsockbynameкешує результати на рівні ОС (принаймні, на моїй машині це робиться). При виклику на раніше невідомий або закінчений термін дії DNS він фактично виконає мережевий запит, який може зайняти деякий час. Якщо виклик на ім'я хоста, яке нещодавно було вирішено, воно відповість набагато швидше. Отже, ваша методологія вимірювання тут недолікована. Це пояснює ваші дивні результати - gevent насправді не може бути набагато гіршим, ніж багатопотокова - обидва не є насправді паралельними на рівні VM.
КТ.

1
@KT. це прекрасний момент. Вам потрібно буде запустити цей тест багато разів і взяти засоби, режими та медіани, щоб отримати гарну картину. Зауважте також, що маршрутизатори кешують маршрути маршрутів для протоколів і там, де вони не кешують маршрути маршруту, ви можете отримати різний відставання від різного маршруту траси маршруту dns. І dns-сервери сильно кешують. Можливо, краще виміряти нарізку, використовуючи time.clock (), де використовуються цикли процесорів, а не затримка через мережеве обладнання. Це може усунути інші послуги ОС, що прокрадаються та додають час для ваших вимірювань.
DevPlayer

О, і ви можете запустити dns flush на рівні ОС між цими трьома тестами, але знову ж таки, що лише зменшить помилкові дані з локального кешування dns.
DevPlayer

Так. Запуск цієї очищеної версії: paste.ubuntu.com/p/pg3KTzT2FG Я отримую майже однакові часи ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe

Я думаю, що OSX робить кешування dns, але для Linux це не "за замовчуванням": stackoverflow.com/a/11021207/34549 , так що так, низькі рівні паралельних змагань стають набагато гіршими за рахунок накладних витрат перекладача
zzzeek
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.