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


141

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

  • 彼らは本当に何が得意ですか?
  • プロキシサーバーで使用するかどうかは良い考えですか?
  • なぜスレッドではないのですか?

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


1
@Imran Javaのグリーンスレッドについてです。私の質問はPythonのGreenletについてです。何か不足していますか?
Rsh

Afaik、グローバルインタープリターロックのため、Pythonのスレッドは実際には同時実行されません。つまり、両方のソリューションのオーバーヘッドを比較することになります。Pythonにはいくつかの実装があることは理解していますが、これはすべての実装に当てはまるとは限りません。
ディディエルク、2013年

3
@didierc CPython(および現在のPyPy)は、Python(バイト)コードを並行して解釈しません(つまり、2つの異なるCPUコアで物理的に同時に解釈しません)。ただし、PythonプログラムのすべてがGILの下にあるわけではなく(一般的な例は、意図的にGILを解放するI / OおよびC関数を含むシステムコールです)、a threading.Threadは実際にはすべての影響を持つOSスレッドです。ですから、それほど単純ではありません。ちなみに、JythonにはGIL AFAIKがなく、PyPyもそれを取り除こうとしています。

回答:


204

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

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

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

Greenletは、先ほど説明した理由により同時実行性を提供します。並行性は並列処理ではありません。イベントの登録を隠し、通常は現在のスレッドをブロックする呼び出しに対してスケジューリングを実行することにより、geventのようなプロジェクトは、非同期APIを変更することなくこの並行性を公開し、システムのコストを大幅に削減します。


1
ありがとう、わずか2つの質問です。1)このソリューションをマルチプロセッシングと組み合わせて、より高いスループットを達成することは可能ですか?2)なぜスレッドを使用するのかまだわかりませんか?それらをpython標準ライブラリでの並行性の素朴で基本的な実装と見なすことはできますか?
Rsh

6
1)はい。これを時期尚早に行うべきではありませんが、この質問の範囲を超える要素がたくさんあるため、複数のプロセスがリクエストを処理すると、スループットが向上します。2)OSスレッドはプリエンプティブにスケジュールされ、デフォルトで完全に並列化されます。Pythonはネイティブスレッドインターフェイスを公開しているため、これらはPythonのデフォルトです。スレッドは、最新のオペレーティングシステムでの並列処理と同時実行性の両方でサポートされる最も優れた共通の分母です。
Matt Joiner 2013年

6
スレッドが不十分になるまでグリーンレットを使用してはならないことにも言及する必要があります(通常、これは、処理している同時接続の数と、スレッド数またはGILのいずれかが悲しみを与えているために発生します)。利用できる他のオプションがない場合のみ。Python標準ライブラリ、およびほとんどのサードパーティライブラリは、スレッドを通じて並行性が実現されることを期待しているため、グリーンレットを介して提供すると、奇妙な動作が発生する可能性があります。
Matt Joiner 2013年

@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

@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にはいくつかの大きな違いがあることがわかります


4
作業を完了するために60000のネイティブスレッドまたはプロセスを生成することは完全に正しくありません。このテストでは何も示されません(gevent.joinall()呼び出しのタイムアウトをオフにしましたか?)。約50スレッドのスレッドプールを使用してみてください。私の答えを参照してください:stackoverflow.com/a/51932442/34549
zzzeek

9

上記の@TemporalBeingの回答を修正すると、グリーンレットはスレッドよりも「高速」ではなく、並行性の問題を解決するために60000スレッドを生成するのは不適切なプログラミング手法であり、代わりに小さなスレッドのプールが適切です。これは、より合理的な比較です(このSOの投稿を引用している人々への返答としての私のredditの投稿から)。

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でのノンブロッキングIOについて誰もが誤解しているのは、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 docは、ネットワーク運用を目的としていると述べています。ネットワーク集中型の操作の場合、スレッド切り替えは問題なく、マルチスレッドアプローチがかなり高速であることがわかります。また、常にpythonの公式ライブラリを使用することをお勧めします。GreenletをWindowsにインストールしようとしたところ、dll依存関係の問題が発生したため、このテストをLinux vmで実行しました。常に、どのマシンでも実行できるようにコードを記述してください。


25
getsockbynameOSレベルで結果をキャッシュすることに注意してください(少なくとも私のマシンでは)。以前は不明または期限切れのDNSで呼び出された場合、実際にはネットワーククエリを実行しますが、これには時間がかかる場合があります。最近解決されたばかりのホスト名で呼び出されると、応答がはるかに速く返されます。したがって、ここでは測定方法に欠陥があります。これはあなたの奇妙な結果を説明します-geventは実際にはマルチスレッドよりもはるかに悪いことはあり得ません-どちらもVMレベルでは実際には並列ではありません。
KT。

1
@KT。それは素晴らしい点です。そのテストを何度も実行し、適切な画像を得るために手段、モード、中央値をとる必要があります。また、ルーターはプロトコルのルートパスをキャッシュし、ルートパスをキャッシュしない場合、異なるDNSルートパストラフィックから異なるラグが発生する可能性があることにも注意してください。また、DNSサーバーは大量にキャッシュします。ネットワークハードウェア上のレイテンシの影響を受けるのではなく、CPUサイクルが使用されるtime.clock()を使用してスレッドを測定する方がよい場合があります。これにより、他のOSサービスが忍び寄り、測定から時間を追加することを排除できます。
DevPlayer 2016年

ああ、3つのテストの間でOSレベルでDNSフラッシュを実行できますが、これもローカルDNSキャッシングからの誤ったデータを減らすだけです。
DevPlayer 2016年

うん。このクリーンアップされたバージョンを実行します。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によるオーバーヘッドインタプリタにそれほど悪化している同時実行greenletsの低水準で、そうそう、
zzzeek
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.