Python Concurrency Tutorial

これは私の PyCon 2020 チュートリアルをまとめたものです。 オリジナルのビデオとソースコードはこちらでご覧いただけます。 https://github.com/santiagobasulto/pycon-concurrency-tutorial-2020

https://www.youtube.com/watch?v=18B1pznaU1o

これは、Pythonを使って効果的に並行プログラムを書く方法に関する簡単なガイド/チュートリアルです。 Python での同時実行は混乱することがあります。 複数のモジュール (threading, _thread, multiprocessing, subprocess) があります。 また、嫌われもののGILがありますが、これはCPythonだけのものです(PyPyとJythonはGILを持っていません)。

このチュートリアルの目的は、同時実行 (または並列) コードの各使用例にどのようにアプローチするかについて、地に足のついた推奨事項を提供することです。 これらの基本的な概念を理解することは、同時実行プログラムを正しく構築するための基礎となります。 すでにこれらのトピックに精通している場合は、これらのセクションをスキップしてください。

このチュートリアルは以下のセクションに分かれています:

  • Concurrency vs Parallelism
  • Computer Architecture
  • The role of the Operating System
  • Threads with Python
  • Thread Synchronization
  • Multiprocessing
  • 高レベルライブラリー(高レベルのライブラリ)の説明です。 concurrent.futuresparallel

同時並行と並列

並列とは、複数のタスクが同時に実行されることです。 並行プログラムの究極の目的である。 並行性は並列性よりも劣り、同じ時間帯に複数のタスクを起動してやりくりしているということです。 料理で言えば、同時並行と並列の例ですね。

Concurrency はキッチンに一人しかいません。

  • Start cutting onions
  • Start heating the pan
  • Finish cutting the onions

このケースでは、同時に複数のことをできないことが明らかです。

Parallelism has multiple people in the kitchen:

  • Person 1 is cutting onions
  • Person 2 is cutting red peppers
  • Person 3 waits for the pan to heat up

In this case are several task being done at the same time…このケースでは、同時に行われる作業がいくつかあることがわかります。

コンピュータ・アーキテクチャ

現代のコンピュータはすべてフォン・ノイマン・アーキテクチャを使って単純化することができ、それは3つの主要な構成要素を持っています。 コンピューティング (CPU)、メモリ (RAM)、I/O (ハードドライブ、ネットワーク、ビデオ出力)です。 ある命令は「計算」(x + 1)、ある命令はメモリ(x = 1)、ある命令は I/O(fp = open('data.csv')) です。

Python における並行処理の詳細を正しく理解するには、これらのコンポーネントの異なる「アクセス時間」を心に留めておくことが重要です。 CPU へのアクセスは、I/O はおろか RAM へのアクセスよりもずっと高速です。 CPUの1サイクルが人間の時間の1秒に相当するとすると、サンフランシスコから香港へのネットワークリクエストは11年かかるという研究結果があります。

Latency at a human scale (source)

これらの「指示」による時間の急激な差はスレッドを議論したり Python GIL に対して基本的なものになるはずです。

オペレーティング システムの役割

私が並行処理に関する大学のコースを作っていたら、オペレーティング システムの歴史をカバーする講義全体を持つことでしょう。 同時実行メカニズムがどのように進化してきたかを理解する唯一の方法です。

最初の消費者向けオペレーティング システム (MS-DOS を考えてください) は、マルチタスクをサポートしていませんでした。 せいぜい、ある時間に 1 つのプログラムだけが実行されるだけでした。 その後、「協調型マルチタスク」システムが登場しました (Windows 3.0)。 このタイプのマルチタスクは、最新のマルチスレッドプログラムというよりも、「イベントループ」(Pythonのasyncioを想像してください)に似ています。 協調型マルチタスクOSでは、アプリケーションは不要になったCPUを「解放」して、他のアプリケーションを実行させる役割を担っています。 すべてのアプリ(あるいは開発者がCPUを解放するタイミングを正しく判断できるほど鋭敏であることも)を信頼することはできないので、これは明らかにうまくいかなかった。 その結果、オペレーティング システムが、どのアプリがどれくらいの頻度でどれくらいの時間 CPU を使用するかを決定するようになりました。 これにより、アプリは 1 つの CPU しか持っていなくても「マルチタスク」であるかのように感じることができます。 OS Scheduler はプロセスを非常にすばやく切り替えています。

次の図では、3 つのプロセス (P1, P2, P3) が同じ CPU を共有して同時に実行されています (青い線が CPU 時間です)。 P1 はかなりの時間実行された後、すぐに P2 に切り替わり、さらに P3 に切り替わり、といった具合です。

3 プロセスは「同時に」実行しています

プロセス

プロセスとは OS がコードの実行に用いる抽象的表現方法です。 このプロセスでコードをラップし、メモリや他の共有リソースを割り当てます。 プロセスの抽象化により、OS は実行中のプログラムを「区別」できるため、互いに競合が発生します。 例えば、プロセス1はプロセス2が予約したメモリにアクセスすることができない。 また、ユーザーのセキュリティの面でも重要です。 ユーザー1がプロセスを起動すると、そのプロセスはそのユーザーがアクセスできるファイルを読むことができます。

次の図は、プロセスの視覚的な表現を含んでいます。 プロセスには、実行しなければならないコード、割り当てられたRAM、およびプログラムによって作成されたすべてのデータ(変数、開いているファイルなど)が含まれています。

A visual representation of an OS Process

ここで、同じ「プログラム」(コードの同じ部分)が複数のプロセスで同時に実行されていることが確認できるのですが、この図では、「プログラム」(コードの小さな部分)は、複数のプロセスで同時実行されています。

ここで第 2 段階に進みます。並行処理 (あるいは並列処理) は実際にどのように実現されるのでしょうか。 開発者としての立場から、「同時実行」を実現するために採用できるメカニズムには、マルチスレッドとマルチプロセシングの 2つがあります。

マルチスレッド

スレッドは、同じプロセス内で同時に実行するフローです。 プロセスは実行の複数のスレッドを開始し、それらは何をしなければならないかに基づいて独立して「進化」します。

たとえば、3 つの Web サイトからデータをダウンロードし、それを組み合わせて最終報告書を作成しなければならないプログラムがあるとします。 各 Web サイトがデータを応答するまでには約 2 秒かかります。 シングルスレッド プログラムは次のようになります:

d1 = get_website_1() # 2 secs
d2 = get_website_2() # 2 secs
d3 = get_website_3() # 2 secscombine(d1, d2, d3) # very fast

総実行時間は >6 秒になります。

A single threaded program

A multithreaded program will start to download the data from the three website simultaneously, and wait until them all has done to make the final report.シングル版のプログラムでは、3つのWebサイトのデータを同時にダウンロードして、すべて終了するのを待ちます。

t1 = Thread(get_website_1)
t2 = Thread(get_website_2)
t3 = Thread(get_website_3)wait_for_threads()d1 = t1.result()
d2 = t2.result()
d3 = t3.result()combine(d1, d2, d3)

3つのWebサイトがほぼ同時に応答するため、現在の実行時間は~2秒に近くなります。

A multithreaded program

Python では、スレッドは threading モジュールから Thread クラスで生成され、私のチュートリアルのレポから最初の notebook でいくつかの例を見てみることができます。 1. 1. スレッドの基本.ipynb.

スレッドと共有データ

スレッドはプログラムによって明示的に作成され、同じプロセス内に存在します。 つまり、プロセスから同じデータをすべて共有します。

スレッドはプロセスデータを共有する

これにより、特に共有データやレースコンディションの予期せぬ変異といったいくつかの問題を引き起こす可能性があります。 ノートブック2にレースコンディションの例があります。 スレッドデータ & レースコンディション.ipynb にレースコンディションの例があります。 同じノートブックで、レースコンディションを回避するためにスレッドを「同期」させるメカニズムをいくつか紹介していますが、同じメカニズムがデッドロックの問題を引き起こす可能性があります。 ノートブックにはデッドロックの例があります。 3. デッドロック.ipynb.

プロデューサー、コンシューマー、キュー

同期の問題は、パラダイムを変更するだけで解決することが可能です。 プロデューサースレッドは実行する「保留タスク」を作成し、それを共有キューに配置する。

Python では、queue モジュールの Queue クラスがこれらの目的のために使用されるスレッドセーフなキューです。 Producer-Consumer model.ipynb.

The Python Global Interpreter Lock

GIL は Python が最も嫌う機能の 1 つです。 GIL は Python インタープリターによって配置される「グローバル」ロックであり、任意の時間に 1 つのスレッドのみが CPU にアクセスできることを保証します。

ここで、コードが CPU の使用についてすべてではないことを理解することが重要です。 コードは、ファイルの読み取り、ネットワークからの情報の取得など、異なるタスクを実行します。 レイテンシーアクセス時間を覚えていますか?

Latency at a human scale (source)

GIL の制限を克服する鍵は、この点です。 スレッドがファイルから何かを読み取る必要がある瞬間に、CPU を解放し、他のスレッドを実行させることができます。 ハードディスクがデータを返すまで、多くの時間がかかります。これは、ほとんどのプログラムがI/Oバウンドと呼ばれるものだからです。 つまり、多くの I/O 機能(ネットワーク、ファイルシステムなど)を使用します。

ノートブックで I/O バウンドタスクの「証明」を見ることができます。 5. Python GIL.ipynb.

マルチプロセシング

多くの計算を行う必要があるプログラムなど、必然的に CPU バウンドになるプログラムもあります。 時には、高速化したいこともあるでしょう。 CPU バウンドのタスクにマルチスレッドを使用すると、GIL のせいでプログラムが速くならない(実際にはさらに遅くなる)ことに気づくでしょう。

並行プログラムを記述するための 2 つ目のメカニズムであるマルチプロセシングに入りましょう。 複数のスレッドを持つ代わりに、実際に複数の子プロセスを生成して、異なるタスクを同時に実行するようにします。

ノートブック 6 に、マルチプロセッシングベースのプログラムの例があります。 Multiprocessing.ipynb

concurrent.futures

最後に、可能であれば使用すべき Python 標準ライブラリの一部である高位モジュールがあることを指摘したいと思います: concurrent.ipynb

concurrent.futures

私のチュートリアルは終了します。futures.

これは、スレッドおよびプロセス プールを作成するための高レベルの抽象化を提供し、可能な限り、同時実行コードを書くためのデフォルトの選択肢と見なされるほど成熟しています。 何か質問があれば、私に知らせてください。 喜んでお答えします。

コメントを残す

メールアドレスが公開されることはありません。