web-dev-qa-db-ja.com

グリーンレット対スレッド

私はgeventsとgreenletsを初めて使用します。それらをどのように使用するかについての良いドキュメントをいくつか見つけましたが、グリーンレットをどのように、いつ使用すべきかについて正当化するものはありませんでした!

  • 彼らは本当に何が得意ですか?
  • プロキシサーバーで使用することをお勧めしますか?
  • なぜスレッドではないのですか?

私が確信していないのは、それらが基本的にコルーチンである場合、どのように並行性を提供できるかということです。

128
Rsh

Greenletsは並行性を提供しますが、並列性はnotです。並行性とは、コードが他のコードとは独立して実行できる場合です。並列処理とは、並行コードを同時に実行することです。並列処理は、ユーザー空間で多くの作業を行う必要がある場合に特に役立ちます。これは通常、CPUに負荷がかかる処理です。並行性は、問題を分解して、異なる部分をより簡単に並行してスケジュールおよび管理できるようにするのに役立ちます。

Greenletsは、1つのソケットとの相互作用が他のソケットとの相互作用とは無関係に発生する可能性があるネットワークプログラミングで非常に優れています。これは並行性の典型的な例です。各グリーンレットは独自のコンテキストで実行されるため、スレッド化せずに同期APIを引き続き使用できます。スレッドは仮想メモリとカーネルオーバーヘッドの点で非常に高価であるため、これは良いことです。そのため、スレッドで達成できる並行性は大幅に低くなります。さらに、Pythonのスレッド化は、GILのために通常よりも高価で制限されています。並行性の代替手段は、通常、Twisted、libevent、libuv、node.jsなどのプロジェクトで、すべてのコードが同じ実行コンテキストを共有し、イベントハンドラーを登録します。

リクエストの処理は独立して実行でき、そのように記述する必要があるため、プロキシの作成にグリーンレット(geventなどを介した適切なネットワークサポート)を使用することをお勧めします。

Greenletsは、前に挙げた理由で並行性を提供します。並行性は並列処理ではありません。通常、現在のスレッドをブロックする呼び出しでイベント登録を隠し、スケジューリングを実行することにより、geventのようなプロジェクトは、非同期APIへの変更を必要とせずに、システムの大幅な低コストでこの並行性を公開します。

182
Matt Joiner

@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になる前にマルチプロセスバージョンが落ちたため、ドロップアウトする必要がありました。しかし、10,000回の繰り返しで:

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

そのため、geventを使用したI/Oには大きな違いがあることがわかります。

18
TemporalBeing

これは分析するのに十分興味深いものです。次に、グリーンレットのパフォーマンスとマルチプロセッシングプールのパフォーマンスとマルチスレッドのパフォーマンスを比較するコードを示します。

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 __== "__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の公式ライブラリを使用することは常に優先されます。 Windowsにgreenletをインストールしようとしましたが、dll依存関係の問題が発生したため、このテストをlinux vmで実行しました。すべてのマシンで実行されることを期待して、常にコードを記述してください。

8
max

上記の@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

誰もがブロックしないIOとPythonについての誤解は、Pythonインタープリターがソケットからの結果を大規模に取得する作業に参加できるという信念ですネットワーク接続自体がIOを返すよりも高速です。確かにこれは一部のケースでは当てはまりますが、Pythonインタープリターは実際には非常に遅いため、人々が考えるほど頻繁には当てはまりません。 ブログの投稿 で、非常に単純なことでも、データベースやDNSサーバーなどへの鮮明で高速なネットワークアクセスを処理している場合、それらのサービスが戻ってくることを示すグラフィカルプロファイルを示しますPythonコードが何千もの接続に対応できるよりもはるかに高速です。

5
zzzeek