Гринлет против. Потоки

142

Я новичок в gevents и greenlets. Я нашел хорошую документацию о том, как с ними работать, но ни одна из них не дала мне обоснования того, как и когда мне следует использовать гринлеты!

  • В чем они действительно хороши?
  • Стоит ли использовать их в прокси-сервере или нет?
  • Почему не темы?

Я не уверен в том, как они могут обеспечить нам параллелизм, если в основном они являются совместными подпрограммами.

Rsh
источник
1
@ Imran Речь идет о зеленых нитях в Java. Мой вопрос касается гринлета в Python. Я что-то упускаю ?
Rsh
Afaik, потоки в python на самом деле не параллельны из-за глобальной блокировки интерпретатора. Таким образом, все сводится к сравнению накладных расходов обоих решений. Хотя я понимаю, что существует несколько реализаций python, поэтому это может не относиться ко всем из них.
didierc
3
@didierc CPython (и PyPy на данный момент) не будет интерпретировать (байтовый) код Python параллельно (то есть фактически физически одновременно на двух разных ядрах ЦП). Однако не все, что делает программа Python, находится под GIL (типичными примерами являются системные вызовы, включая функции ввода-вывода и C, которые намеренно освобождают GIL), а a threading.Threadфактически является потоком ОС со всеми ответвлениями. Так что на самом деле все не так просто. Кстати, у Jython нет GIL AFAIK, и PyPy тоже пытается от него избавиться.

Ответы:

210

Greenlets обеспечивают параллелизм, но не параллелизм. Параллелизм - это когда код может работать независимо от другого кода. Параллелизм - это одновременное выполнение параллельного кода. Параллелизм особенно полезен, когда в пользовательском пространстве нужно проделать много работы, а это, как правило, загружает процессор. Параллелизм полезен для разделения проблем, позволяя планировать и управлять различными частями параллельно.

Greenlets действительно блестят в сетевом программировании, где взаимодействие с одним сокетом может происходить независимо от взаимодействия с другими сокетами. Это классический пример параллелизма. Поскольку каждый гринлет выполняется в собственном контексте, вы можете продолжать использовать синхронные API без потоковой передачи. Это хорошо, потому что потоки очень дороги с точки зрения виртуальной памяти и накладных расходов ядра, поэтому параллелизм, которого можно достичь с помощью потоков, значительно меньше. Кроме того, многопоточность в Python дороже и более ограничена, чем обычно, из-за GIL. Альтернативой параллелизму обычно являются такие проекты, как Twisted, libevent, libuv, node.js и т. Д., Где весь ваш код использует один и тот же контекст выполнения и регистрирует обработчики событий.

Это отличная идея - использовать гринлеты (с соответствующей сетевой поддержкой, например, через gevent) для написания прокси, поскольку ваша обработка запросов может выполняться независимо и должна быть написана как таковая.

Greenlets обеспечивают параллелизм по причинам, о которых я говорил ранее. Параллелизм - это не параллелизм. Скрывая регистрацию событий и выполняя планирование вызовов, которые обычно блокируют текущий поток, такие проекты, как gevent, раскрывают этот параллелизм, не требуя изменений в асинхронном API, и при значительно меньших затратах для вашей системы.

Мэтт Джойнер
источник
1
Спасибо, всего два небольших вопроса: 1) Можно ли совместить это решение с многопроцессорностью для достижения большей пропускной способности? 2) Я до сих пор не знаю, зачем вообще использовать потоки? Можно ли рассматривать их как наивную и базовую реализацию параллелизма в стандартной библиотеке Python?
Rsh
6
1) Да, конечно. Вы не должны делать это преждевременно, но из-за целого ряда факторов, выходящих за рамки этого вопроса, наличие нескольких процессов, обслуживающих запросы, даст вам более высокую пропускную способность. 2) Потоки ОС заранее запланированы и по умолчанию полностью распараллелены. Они используются по умолчанию в Python, поскольку Python предоставляет собственный интерфейс потоковой передачи, а потоки являются наиболее поддерживаемым и наименьшим общим знаменателем как для параллелизма, так и для параллелизма в современных операционных системах.
Matt Joiner
6
Я должен упомянуть, что вы даже не должны использовать гринлеты до тех пор, пока потоки не станут удовлетворительными (обычно это происходит из-за количества одновременных подключений, которые вы обрабатываете, и либо количество потоков, либо GIL причиняют вам боль), и даже то только в том случае, если вам не доступен какой-либо другой вариант. Стандартная библиотека Python и большинство сторонних библиотек ожидают, что параллелизм будет достигнут через потоки, поэтому вы можете получить странное поведение, если предоставите это через гринлеты.
Matt Joiner
@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()
Сумья
17

Взяв ответ @Max и добавив к нему некоторую релевантность для масштабирования, вы можете увидеть разницу. Я добился этого, изменив 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

TemporalBeing
источник
7
Совершенно неправильно создавать 60000 собственных потоков или процессов для завершения работы, и этот тест ничего не показывает (также вы сняли тайм-аут вызова gevent.joinall ()?). Попробуйте использовать пул потоков из примерно 50 потоков, см. Мой ответ: stackoverflow.com/a/51932442/34549
zzzeek
12

Исправляя ответ @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 может выполнять работу по извлечению результатов из сокетов в большом масштабе быстрее, чем сами сетевые соединения могут возвращать ввод-вывод. Хотя это, безусловно, верно в некоторых случаях, это не так часто, как думают люди, потому что интерпретатор Python действительно очень медленный. В своем сообщении в блоге я проиллюстрирую некоторые графические профили, которые показывают, что даже для очень простых вещей, если вы имеете дело с четким и быстрым сетевым доступом к таким вещам, как базы данных или DNS-серверы, эти службы могут возвращаться намного быстрее, чем код Python. может поддерживать многие тысячи таких связей.

Zzzeek
источник
7

Это достаточно интересно для анализа. Вот код для сравнения производительности гринлетов в сравнении с многопроцессорным пулом и многопоточностью:

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

Я думаю, что гринлет утверждает, что он не связан с GIL, в отличие от библиотеки многопоточности. Более того, в документе Greenlet говорится, что он предназначен для сетевых операций. Для операций, интенсивно использующих сеть, переключение потоков подходит, и вы можете видеть, что многопоточный подход работает довольно быстро. Также всегда предпочтительнее использовать официальные библиотеки python; Я попытался установить greenlet в Windows и столкнулся с проблемой зависимости dll, поэтому я провел этот тест на Linux vm. Всегда пытайтесь писать код в надежде, что он будет работать на любой машине.

Максимум
источник
25
Обратите внимание, что getsockbynameкэширует результаты на уровне ОС (по крайней мере, на моей машине это происходит). При вызове на ранее неизвестном или просроченном DNS он фактически выполняет сетевой запрос, который может занять некоторое время. При вызове для имени хоста, которое недавно было разрешено, он вернет ответ намного быстрее. Следовательно, ваша методология измерения здесь ошибочна. Это объясняет ваши странные результаты - gevent на самом деле не может быть намного хуже, чем многопоточность - оба не совсем параллельны на уровне виртуальной машины.
КТ.
1
@KT. это отличный момент. Вам нужно будет выполнить этот тест много раз и использовать средства, режимы и медианы, чтобы получить хорошее изображение. Также обратите внимание, что маршрутизаторы кэшируют пути маршрутов для протоколов, и там, где они не кэшируют маршруты маршрутов, вы можете получить различную задержку от трафика разных путей маршрута DNS. И днс сервера сильно кешируют. Возможно, лучше измерить потоки с помощью time.clock (), где используются циклы процессора, а не задержка на сетевом оборудовании. Это может исключить проникновение других служб ОС и добавление времени к вашим измерениям.
DevPlayer
Да, и вы можете запустить очистку DNS на уровне ОС между этими тремя тестами, но опять же, это уменьшит ложные данные от локального кеширования DNS.
DevPlayer
Ага. Запустив эту очищенную версию: paste.ubuntu.com/p/pg3KTzT2FG У меня почти идентичные времена ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
см
Я думаю, что OSX выполняет кеширование DNS, но в Linux это не "по умолчанию": stackoverflow.com/a/11021207/34549 , так что да, на низких уровнях параллелизма гринлеты намного хуже из-за накладных расходов интерпретатора
zzzeek