マルチプロセスとコルーチン

目次

1.イテレータ

2.ジェネレータ

3.コルーチン-yield

4.コルーチン-greenlet

5.コルーチン-gevent

6.プロセス、スレッド、コルーチンの違い

7.ケーススタディ:並列ダウンローダー

一.イテレータ

イテレーションはコレクションの要素にアクセスする一つの方法です。イテレータは、イテレーションの位置を記憶できるオブジェクトです。イテレータオブジェクトはコレクションの最初の要素からアクセスを開始し、すべての要素がアクセスされるまで続きます。イテレータは前に進むだけで、後ろには戻れません。

1. イテラブルオブジェクト

既に、list、tuple、strなどのデータ型でfor...in...ループ構文を使用してデータを順に取得し使用できることを知っています。このようなプロセスを反復(イテレーション)と呼びます。

しかし、すべてのデータ型がfor...in...文の中に置かれ、for...in...が毎回データを一つ取り出して私たちが使用できるように、すなわち反復可能であるでしょうか?

>>> for i in 100:
...     print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
# int型はイテラブルではなく、つまりint型は反復不可能です

# データを格納するための独自コンテナMyListを定義し、addメソッドでデータを追加できます
>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...
>>> mylist = MyList()
>>> mylist.add(1)
>>> mylist.add(2)
>>> mylist.add(3)
>>> for num in mylist:
...     print(num)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'MyList' object is not iterable
>>>
# MyListコンテナのオブジェクトも反復不可能です

独自のコンテナ型MyListを定義し、複数のデータを格納したMyListオブジェクトをfor...in...文に入れてみましたが、for...in...が中からデータを一つずつ取り出して返してくれないことがわかりました。つまり、複数のデータを格納できる型を簡単にラップしただけでは、反復して使用することはできないのです。

for...in...のような文でデータを一つずつ読み取って使用できるオブジェクトを、イテラブルオブジェクト(Iterable)と呼びます。

2. オブジェクトが反復可能かどうかを判断する方法

isinstance()を使用してオブジェクトがIterableオブジェクトかどうかを判断できます:

In [50]: from collections import Iterable

In [51]: isinstance([], Iterable)
Out[51]: True

In [52]: isinstance({}, Iterable)
Out[52]: True

In [53]: isinstance('abc', Iterable)
Out[53]: True

In [54]: isinstance(mylist, Iterable)
Out[54]: False

In [55]: isinstance(100, Iterable)
Out[55]: False

3. イテラブルオブジェクトの本質

イテラブルオブジェクトを反復使用するプロセスを分析すると、反復するたびに(for...in...のループごとに)オブジェクトの次のデータを返し、すべてのデータを反復し終わったら終了することがわかります。このプロセスでは、どのデータにアクセスしているかを記録する「人」が必要です。これにより、反復ごとに次のデータを返すことができます。このデータ反復を助けてくれる「人」をイテレータ(Iterator)と呼びます。

イテラブルオブジェクトの本質は、反復走査の助けとなる中間の「人」、つまりイテレータを提供できることです。

イテラブルオブジェクトは__iter__メソッドを通じてイテレータを提供し、イテラブルオブジェクトを反復するとき、実際にはまずそのオブジェクトが提供するイテレータを取得し、そのイテレータを通じてオブジェクトの各データを順に取得します。

つまり、__iter__メソッドを持つオブジェクトは、イテラブルオブジェクトということになります。

>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...     def __iter__(self):
...             """イテレータを返す"""
...             # イテレータオブジェクトの構築方法は一旦無視します
...             pass
...
>>> mylist = MyList()
>>> from collections import Iterable
>>> isinstance(mylist, Iterable)
True
>>>
# 今回、__iter__メソッドを追加したmylistオブジェクトがイテラブルオブジェクトであることがテストで確認できます

4. iter()関数とnext()関数

list、tupleなどはすべてイテラブルオブジェクトであり、iter()関数を使用してこれらのイテラブルオブジェクトのイテレータを取得できます。そして、取得したイテレータに対してnext()関数を繰り返し使用して次のデータを取得できます。iter()関数は、実際にはイテラブルオブジェクトの__iter__メソッドを呼び出しています。

>>> li = [11, 22, 33, 44, 55]
>>> li_iter = iter(li)
>>> next(li_iter)
11
>>> next(li_iter)
22
>>> next(li_iter)
33
>>> next(li_iter)
44
>>> next(li_iter)
55
>>> next(li_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

注意点として、最後のデータを反復し終えた後、next()関数を再度呼び出すと、すべてのデータが反復完了したことを伝えるStopIteration例外がスローされます。

5. オブジェクトがイテレータかどうかを判断する方法

isinstance()を使用してオブジェクトがIteratorオブジェクトかどうかを判断できます:

In [56]: from collections import Iterator

In [57]: isinstance([], Iterator)
Out[57]: False

In [58]: isinstance(iter([]), Iterator)
Out[58]: True

In [59]: isinstance(iter("abc"), Iterator)
Out[59]: True

6. イテレータIterator

上記の分析を通じて、イテレータは反復ごとのアクセス位置を記録するために役立つものであることがわかりました。イテレータにnext()関数を使用すると、イテレータは記録された位置の次の位置のデータを返します。実際には、next()関数を使用する際には、イテレータオブジェクトの__next__メソッド(Python3では__next__メソッド、Python2ではnext()メソッド)が呼び出されます。したがって、イテレータを構築するには、__next__メソッドを実装する必要があります。しかし、これだけでは不十分です。Pythonはイテレータ自体もイテラブルであることを要求するため、イテレータにも__iter__メソッドを実装する必要があります。そして、__iter__メソッドはイテレータを返す必要がありますが、イテレータ自体がイテレータであるため、イテレータの__iter__メソッドは自身を返すだけです。

__iter__メソッドと__next__メソッドを実装したオブジェクトが、イテレータです。

class MyList(object):
    """カスタムイテラブルオブジェクト"""
    def __init__(self):
        self.items = []

    def add(self, val):
        self.items.append(val)

    def __iter__(self):
        myiterator = MyIterator(self)
        return myiterator


class MyIterator(object):
    """上記のイテラブルオブジェクトで使用するカスタムイテレータ"""
    def __init__(self, mylist):
        self.mylist = mylist
        # currentは現在アクセスしている位置を記録するために使用します
        self.current = 0

    def __next__(self):
        if self.current < len(self.mylist.items):
            item = self.mylist.items[self.current]
            self.current += 1
            return item
        else:
            raise StopIteration

    def __iter__(self):
        return self


if __name__ == '__main__':
    mylist = MyList()
    mylist.add(1)
    mylist.add(2)
    mylist.add(3)
    mylist.add(4)
    mylist.add(5)
    for num in mylist:
        print(num)

7. for...in...ループの本質

for item in Iterableループの本質は、まずiter()関数を使用してイテラブルオブジェクトIterableのイテレータを取得し、次に取得したイテレータに対してnext()メソッドを繰り返し呼び出して次の値を取得し、それをitemに代入することです。StopIteration例外が発生するとループが終了します。

8. イテレータの適用シーン

イテレータの最も重要な機能は、next()関数の呼び出しによって次のデータ値を返すことです。返されるデータ値が既存のデータセットから読み取られるのではなく、プログラムが一定の規則に従って計算して生成される場合、既存のデータセットに依存する必要がなくなります。つまり、反復するすべてのデータを一度にキャッシュして後から順に読み取る必要がなくなり、これにより大量のストレージ(メモリ)空間を節約できます。

例えば、数学には有名なフィボナッチ数列があります。数列の最初の数は0、2番目の数は1、その後の各数は前の2つの数を足して得られます:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

今、for...in...ループを使用してフィボナッチ数列の最初のn個の数を反復処理したいとします。このフィボナッチ数列は、イテレータを使用して実装できます。反復ごとに数学計算によって次の数を生成します。

class FibIterator(object):
    """フィボナッチ数列イテレータ"""
    def __init__(self, n):
        """
        :param n: int, 数列の最初のn個の数を生成する
        """
        self.n = n
        # currentは現在数列の何番目まで生成したかを保存するために使用します
        self.current = 0
        # num1は前々の数を保存するために使用し、初期値は数列の最初の数0です
        self.num1 = 0
        # num2は前の数を保存するために使用し、初期値は数列の2番目の数1です
        self.num2 = 1

    def __next__(self):
        """next()関数によって次の数を取得するために呼び出されます"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """イテレータの__iter__は自身を返すだけです"""
        return self


if __name__ == '__main__':
    fib = FibIterator(10)
    for num in fib:
        print(num, end=" ")

9. forループだけがイテラブルオブジェクトを受け取れるわけではない

forループだけでなく、list、tupleなどもイテラブルオブジェクトを受け取ることができます。

li = list(FibIterator(15))
print(li)
tp = tuple(FibIterator(6))
print(tp)

二.ジェネレータ

1. ジェネレータ

イテレータを使用すると、next()メソッドでデータを取得するたびに特定の規則に従って生成できます。しかし、イテレータを実装する際に、現在反復している状態を自分で記録する必要があり、その状態に基づいて次のデータを生成する必要があります。現在の状態を記録し、next()関数と連携して反復使用するために、より簡単な構文であるジェネレータ(generator)を使用できます。ジェネレータは特殊な種類のイテレータです。

2. ジェネレータの作成方法1

ジェネレータを作成するには、いくつかの方法があります。最初の方法は非常に簡単で、リスト内包表記の [ ] を ( ) に変更するだけです。

In [15]: L = [ x*2 for x in range(5)]

In [16]: L
Out[16]: [0, 2, 4, 6, 8]

In [17]: G = ( x*2 for x in range(5))

In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>

In [19]:

LとGを作成する違いは最外層の [ ] と ( ) のみです。Lはリストで、Gはジェネレータです。リストLの各要素を直接出力できますが、ジェネレータGについては、イテレータの使用方法に従って使用できます。つまり、next()関数、forループ、list()などの方法を使用できます。

In [19]: next(G)
Out[19]: 0

In [20]: next(G)
Out[20]: 2

In [21]: next(G)
Out[21]: 4

In [22]: next(G)
Out[22]: 6

In [23]: next(G)
Out[23]: 8

In [24]: next(G)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(G)

StopIteration:

In [25]:
In [26]: G = ( x*2 for x in range(5))

In [27]: for x in G:
   ....:     print(x)
   ....:     
0
2
4
6
8

In [28]:
  1. ジェネレータの作成方法2

ジェネレータは非常に強力です。計算アルゴリズムが複雑で、リスト内包表記のようなforループで実現できない場合は、関数を使用して実現できます。

前のセクションで説明したフィボナッチ数列を例に戻り、前のセクションでのイテレータでの実装方法を思い出しましょう:

class FibIterator(object):
    """フィボナッチ数列イテレータ"""
    def __init__(self, n):
        """
        :param n: int, 数列の最初のn個の数を生成する
        """
        self.n = n
        # currentは現在数列の何番目まで生成したかを保存するために使用します
        self.current = 0
        # num1は前々の数を保存するために使用し、初期値は数列の最初の数0です
        self.num1 = 0
        # num2は前の数を保存するために使用し、初期値は数列の2番目の数1です
        self.num2 = 1

    def __next__(self):
        """next()関数によって次の数を取得するために呼び出されます"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """イテレータの__iter__は自身を返すだけです"""
        return self

イテレータを使用した実装方法では、反復の状態を保存するためにいくつかの変数(n、current、num1、num2)を使用する必要があることに注意してください。ここで、ジェネレータを使用して実装してみましょう。

In [30]: def fib(n):
   ....:     current = 0
   ....:     num1, num2 = 0, 1
   ....:     while current < n:
   ....:         num = num1
   ....:         num1, num2 = num2, num1+num2
   ....:         current += 1
   ....:         yield num
   ....:     return 'done'
   ....:

In [31]: F = fib(5)

In [32]: next(F)
Out[32]: 1

In [33]: next(F)
Out[33]: 1

In [34]: next(F)
Out[34]: 2

In [35]: next(F)
Out[35]: 3

In [36]: next(F)
Out[36]: 5

In [37]: next(F)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-37-8c2b02b4361a> in <module()>
----> 1 next(F)

StopIteration: done

ジェネレータを使用した実装方法では、元々イテレータの__next__メソッドで実装していた基本的なロジックを関数内に実装しますが、反復ごとに値を返すreturnをyieldに変更します。このようにして定義された関数はもはや関数ではなく、ジェネレータになります。簡単に言えば:yieldキーワードを使用した関数はジェネレータです。

このようにジェネレータを使用する場合(例ではF = fib(5))、関数を呼び出すのではなく、ジェネレータオブジェクト(例ではF)が返されます。そして、そのジェネレータをイテレータとして使用できます。

In [38]: for n in fib(5):
   ....:     print(n)
   ....:     
1
1
2
3
5

In [39]:

しかし、forループでジェネレータを呼び出すと、ジェネレータのreturn文の戻り値が取得できません。戻り値を取得するには、StopIteration例外を捕捉する必要があります。戻り値はStopIterationのvalueに含まれています:

In [39]: g = fib(5)

In [40]: while True:
   ....:     try:
   ....:         x = next(g)
   ....:         print("value:%d"%x)      
   ....:     except StopIteration as e:
   ....:         print("ジェネレータの戻り値:%s"%e.value)
   ....:         break
   ....:     
value:1
value:1
value:2
value:3
value:5
ジェネレータの戻り値:done

In [41]:

まとめ

  • yieldキーワードを使用した関数は、もはや関数ではなく、ジェネレータになります。(yieldを使用した関数はジェネレータです)
  • yieldキーワードには2つの役割があります:
  • next()関数を使用してジェネレータを中断点から実行を再開させることができます。つまり、ジェネレータ(関数)を起動します
  • 現在の実行状態(中断点)を保存し、実行を一時停止します。つまり、ジェネレータ(関数)を中断します
  • yieldキーワードの後の式の値を戻り値として返し、このときreturnと同様の役割を果たします
  • Python3のジェネレータはreturnを使用して最終的な戻り値を返すことができますが、Python2のジェネレータはreturnで戻り値を返すことは許可されていません(returnでジェネレータから退出できますが、returnの後に式を置くことはできません)。

4. sendを使用した起動

next()関数を使用してジェネレータを中断点から実行を再開させるだけでなく、send()関数を使用して起動することもできます。send()関数の利点は、中断点に追加データを渡しながら起動できることです。

例:yieldに到達したとき、gen関数は一時的に保存され、iの値が返されます。tempは次のc.send("python")で送信されてきた値を受け取り、c.next()はc.send(None)と同等です。

In [10]: def gen():
   ....:     i = 0
   ....:     while i<5:
   ....:         temp = yield i
   ....:         print(temp)
   ....:         i+=1
   ....:

sendの使用

In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

next関数の使用

In [11]: f = gen()

In [12]: next(f)
Out[12]: 0

In [13]: next(f)
None
Out[13]: 1

In [14]: next(f)
None
Out[14]: 2

In [15]: next(f)
None
Out[15]: 3

In [16]: next(f)
None
Out[16]: 4

In [17]: next(f)
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)

StopIteration:

__next__()メソッドの使用(一般的ではありません)

In [18]: f = gen()

In [19]: f.__next__()
Out[19]: 0

In [20]: f.__next__()
None
Out[20]: 1

In [21]: f.__next__()
None
Out[21]: 2

In [22]: f.__next__()
None
Out[22]: 3

In [23]: f.__next__()
None
Out[23]: 4

In [24]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()

StopIteration:

三.コルーチン

コルーチン、別名マイクロスレッド、ファイバー。英名Coroutine。

コルーチンとは

コルーチンはPythonでマルチタスクを実現する別の方法ですが、スレッドよりも小さな実行単位(必要なリソースが少ない)を占めます。なぜ実行単位と呼ぶのか、それはCPUコンテキストを内蔵しているからです。適切なタイミングで、あるコルーチンを別のコルーチンに切り替えることができます。このプロセスでCPUコンテキストを保存または復元できる限り、プログラムは実行し続けられます。

平易な理解:あるスレッド内の特定の関数が、任意の場所で現在の関数の一時的な変数などの情報を保存し、別の関数に切り替えて実行することができます。ただし、関数を呼び出す方法ではなく、そしていつもとの関数に戻るかは開発者が自分で決定します。

コルーチンとスレッドの違い

マルチタスクを実現する際、スレッドの切り替えはシステムレベルでCPUコンテキストの保存と復元だけではありません。プログラムの実行効率を高めるために、各スレッドには独自のキャッシュなどのデータがあります。オペレーティングシステムはこれらのデータの復元操作も行います。そのため、スレッドの切り替えは非常にコストがかかります。しかし、コルーチンの切り替えは単純にCPUコンテキストを操作するだけなので、1秒に数百万回切り替えてもシステムは対応できます。

コルーチンの簡単な実装

import time

def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)

def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

実行結果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2----
----work1---
----work2----
----work1---
----work2----
----work1---
----work2----
...省略...

四.greenlet

コルーチンを使用してマルチタスクをより良く実現するために、Pythonのgreenletモジュールがそれをカプセル化し、タスクの切り替えをより簡単にします。

インストール方法

以下のコマンドでgreenletモジュールをインストールします:

sudo pip3 install greenlet
#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#gr1に切り替えて実行
gr1.switch()

実行結果

---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

五.gevent

greenletはすでにコルーチンを実現していますが、この切り替えは手動で行う必要があり、面倒に感じるかもしれません。心配しないでください、greenletより強力で、タスクを自動的に切り替えることができるモジュールgeventがPythonにはもう一つあります。

その原理は、greenletがIO(入出力、ネットワーク、ファイル操作など)操作に遭遇したとき、例えばネットワークにアクセスすると、自動的に他のgreenletに切り替わり、IO操作が完了したら、適切なタイミングで切り替わってきて実行を再開します。

IO操作は非常に時間がかかり、頻繁にプログラムを待機状態にすることがあります。geventが私たちのために自動的にコルーチンを切り替えてくれることで、常にgreenletが実行中であり、IOを待っているわけないことが保証されます。

インストール

pip3 install gevent

1. geventの使用

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

実行結果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

3つのgreenletが交互に実行されているのではなく、順次実行されていることがわかります。

2. geventによる切り替え実行

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #timeモジュールのsleepではなく、時間のかかる操作をシミュレートするために使用します
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

実行結果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

3. プログラムにパッチを適用

from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

実行結果

work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
from gevent import monkey
import gevent
import random
import time

# 時間のかかる操作がある場合に必要です
monkey.patch_all()  # プログラムで使用される時間のかかる操作のコードをgeventが独自に実装したモジュールに置き換えます

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

実行結果

work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9

六.プロセス、スレッド、コルーチンの比較

以下の平易な説明をよく理解してください

  • ある経営者がある商品(例えばハサミ)を生産するために工場を開設したいと考えています
  • 彼は生産ラインを作るために財力と物力を費やし、その生産ラインには多くのデバイスと材料があり、これらすべてがハサミを生産するために準備されたリソースと呼ばれます:プロセス
  • 生産ラインだけでは生産できないため、経営者は作業をするために労働者を雇う必要があります。この労働者はこれらの資材を利用して最終的に段階的にハサミを作ることができ、このことを行う労働者は:スレッドと呼ばれます
  • この経営者は生産率を向上させるために3つの方法を思いつきました:
  1. この生産ラインに多くの労働者を雇い、一緒にハサミを作ることで、効率は倍増します。つまり、シングルプロセス・マルチスレッド方式です
  2. 経営者は、この生産ラインの労働者が多ければ多いほど良いわけではないことに気づきます。なぜなら、生産ラインのリソースと材料は限られているからです。そのため、経営者はさらに財力と物力を費やして別の生産ラインを購入し、さらに労働者を雇いました。これにより、効率がさらに向上しました。つまり、マルチプロセス・マルチスレッド方式です
  3. 経営者は、すでに多くの生産ラインがあり、各生産ラインには多くの労働者がいる(つまりプログラムはマルチプロセスで、各プロセスには複数のスレッドがある)ことに気づきました。さらに効率を向上させるために、経営者は損な策を考えました:ある従業員が仕事中に一時的に何もしないか、または特定の条件を待っている場合(例えば、別の労働者が特定の工程を完成させてから自分が再び仕事をできるようになるまで待つ)、その従業員はその時間を使って他のことをするように規定しました。つまり、あるスレッドが特定の条件を待っている場合、その時間を他のことに利用できます。実際には、これは:コルーチン方式です

簡単なまとめ

  1. プロセスはリソース割り当ての単位です
  2. スレッドはオペレーティングシステムがスケジュールする単位です
  3. プロセスの切り替えには非常に多くのリソースが必要で、効率が低いです
  4. スレッドの切り替えには一般的なリソースが必要で、効率は一般的です(もちろんGILを考慮しない場合)
  5. コルーチンの切り替えには非常に少ないリソースが必要で、効率が高いです
  6. マルチプロセス、マルチスレッドはCPUコア数によって並列である可能性がありますが、コルーチンは一つのスレッド内で実行されるため、並列ではなく逐次的です

七.並列ダウンローダー

並列ダウンロードの原理

from gevent import monkey
import gevent
import urllib.request

# 時間のかかる操作がある場合に必要です
monkey.patch_all()

def my_downLoad(url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, 'http://www.baidu.com/'),
        gevent.spawn(my_downLoad, 'http://www.itcast.cn/'),
        gevent.spawn(my_downLoad, 'http://www.itheima.com/'),
])

実行結果

GET: http://www.baidu.com/
GET: http://www.itcast.cn/
GET: http://www.itheima.com/
111327 bytes received from http://www.baidu.com/.
172054 bytes received from http://www.itheima.com/.
215035 bytes received from http://www.itcast.cn/.

上記から、baiduの情報を取得するリクエストが最初に送信され、次にitcast、itheimaの順であることがわかります。しかし、データを受信する順序は送信順序と必ずしも同じではなく、これが非同期性を示しています。つまり、いつデータを受信するかは確定せず、順序も一定ではないということです。

複数のビデオのダウンロードの実装

from gevent import monkey
import gevent
import urllib.request

#IO操作がある場合にこの一文が必要です
monkey.patch_all()

def my_downLoad(file_name, url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()

    with open(file_name, "wb") as f:
        f.write(data)

    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, "1.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-08-%E3%80%90%E7%90%86%E8%A7%A3%E3%80%91%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89.mp4'),
        gevent.spawn(my_downLoad, "2.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-03-%E3%80%90%E6%8E%8C%E6%8F%A1%E3%80%91%E6%97%A0%E5%8F%82%E6%95%B0%E6%97%A0%E8%BF%94%E5%9B%9E%E5%80%BC%E5%87%BD%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%80%81%E8%AA%BF%E7%94%A8%28%E4%B8%8B%29.mp4'),
])

上記のURLは、ダウンロードしたいビデオ、音楽、画像などのURLに変更できます。

タグ: Python マルチプロセス コルーチン ジェネレータ greenlet

7月10日 00:13 投稿