増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編
目次
- 目次
- はじめに
- 各言語でマルチスレッドプログラミング
- マルチスレッドパターン
- マルチスレッドプログラミングで注意すべきこと
- 参考資料
- MyEnigma Supporters
はじめに
最近は、マルチプロセスで複数のプロセスを協調させる
マイクロサービスが利用されることが多いですが、
GUIやハードウェアアクセスがあるソフトウェアでは、
未だにマルチスレッドプログラミングをしないといけない時も多いです。
今回は、冒頭の本を元に
様々な言語でマルチスレッドプログラミングを
実施する際の、メモを残しておきたいと思います。
各言語でマルチスレッドプログラミング
各言語でマルチスレッドを実施する時の概要についてまとめておきます。
Java
Javaは様々な言語の中でも、
かなりマルチスレッドプログラミングがしやすい言語だと思います。
複数のスレッドで共有されるフィールドは
synchronizedやvolatileで守ることで、マルチスレッドプログラミングが可能です。
また、java.util.concurrentという標準ライブラリを使うことで、
よく使用するマルチスレッドプログラミングをしやすくなっています。
Javaにおけるマルチスレッドプログラミングの基本に関しては、
冒頭の書籍を参考してください。
Python
Pythonには、マルチスレッド用の標準ライブラリとして、
threadingがあります。
こちらを使うことで、JavaのThreadクラスのようにスレッドを生成したり、
synchronizedのような排他処理が可能になります。
(そもそも、こちらのライブラリは、
Javaのスレッドライブラリを元に、設計されたようです。)
特に、Conditionsオブジェクトを使うことで、Javaのような
synchronized, wait, notify, notify_all などが実現できるため、
Javaのマルチスレッドプログラミングに慣れている人にはおすすめです。
Pythonのスレッドの注意点としては、
PythonはGlobal Interpreter Lock:GILというメモリ管理機能により、
マルチスレッドのプログラミングをしても、
複数のコアを使いきるような処理は出来ません。
あくまでもI/O待ちなどの時に、
スループットを向上させる用途がメインです。
このような場合は、
並行処理ではなく、マルチプロセスによる並列処理をすることで、
マルチコアを使い切る処理ができます。
マルチスレッドパターン
下記は、記事冒頭の書籍を元に、
各マルチスレッドプログラミングの代表的なパターンを、
各言語で実装したものです。
1. Single-Threaded Executionパターン
このパターンは、複数のスレッドがあるときに、
ある特定の処理は、複数のスレッドから
同時に実施されないようにするパターンです。
Javaコード
Pythonコード
2. Immutableパターン
このパターンは、
複数のスレッドであるリソースにアクセスしても大丈夫なように、
あるクラスのフィールドを不変(Immutable)にするパターンです。
Pythonコード
Pythonは、JavaやC++のように、finalやconstで
完全にクラスのフィールドをImmutableにするのが難しいため、
propertyマクロで、変更出来ないようにしています。
Javaコード
3. guarded_suspensionパターン
このパターンは、
ある条件が満たされるまで、
スレッドを待たせるパターンです。
Javaコード
Pythonコード
Pythonでは、threading.Conditionが、
waitやnotify, notify_allの関数を持つため、
Javaと同じような形でスレッドプログラミングを実現できます。
4. Balkingパターン
このパターンは、ある条件に達してない場合は、
スレッドの処理をやめさせるパターンです。
Java コード
Python
Pythonでも、get_thread()して、現在のthreadのオブジェクトを取得し、
nameフィールドで、threadの名前を取得できます。
5. Producer-Consumer
データを作成するスレッドと、
そのデータを利用するスレッドが別れているときに、
間に橋渡しのクラスを作って、
安全にデータをやり取りするパターンです。
それぞれの処理速度のずれを吸収することができます。
Javaコード
Pythonコード
Pythonでは、Producer-Consumerパターン用のキュークラスとして、
下記の標準ライブラリが準備されています。
上記のライブラリを使った、例が下記のサンプルコードです。
ちなみにマルチスレッドではなく、マルチプロセス用のqueueも準備されており、
これを利用した並列処理の例は下記の記事で紹介しています。
6. Read Write Lock
あるリソースに対して、
Read同士は同時にできるが、
Writeは排他制御されるパターンです。
リソースをWriteする頻度は少ないが、
Readする頻度が高い時に、Readの排他制御を無くすことで、
スループットを改善することができます。
Java
Python
7. Thread per messageパターン
一つの要求ごとに、新しいthreadをつくるパターンです。
リクエストが終わる前に、
メインのスレッドが動き始められるので、応答性が高くすることができます。
しかし、このパターンは、返り値を受け取らない場合のみ利用できます。
返り値が必要な場合は後述のFutureパターンを使います。
Java
Python
8. Worker Thread
Thread per messageは一つの要求毎に、
スレッドを作っていましたが、
スレッドを作るのは結構、
Worker Threadは最初に生成した複数のTheradを使い回す。
Java
Python
9. Future (promise)
ある処理を別のスレッドで実施し、その返り値も必要な場合、
まず要求を出して、future(先物)をもらい、
別のプロセスで返り値を計算しつつ、
そのfutureを元に、後ほど返り値をもらうパターンです。
このパターンはpromise(約束)と呼ばれることがあります。
Java
Javaでこのfutureを実装するときには、
java.util.concurrent.Callable
java.util.concurrent.Future
java.util.concurrent.FutureTask
java.util.concurrent.CompletableFuture;
などを使って、実装されます。
CompletableFutureとFutureの違いに関しては、こちらも参照ください。
Python
Pythonでは、マルチスレッドでも、マルチプロセスでも、
下記のconcurrent.futuresモジュールを使います。
下記がマルチスレッドにおけるfutureパターンのサンプルコードです。
上記のコードのように、
javaのinterfaceのようなコードは、
abc (抽象基底クラス)で実現できます。
また、マルチプロセスにおけるFutureパターンは
下記の記事で紹介しています。
10. Two Phase Termination
このパターンは、スレッドで仕事をして、
スレッドを止めた後に、
終了処理を実施するパターンです。
Java
Python
下記がPythonのサンプルコードです。
前述の通り、PythonはGILがあるため、2つのスレッドが同時に処理をすることはありません。
ですので、Javaのvolatileのような処理は不要です。
また、Pythonのthreadingモジュールは、
自分自身のthreadを止めるinterruptのような関数も実装されていません。
11. Thread Specific Storage
このパターンは、javaのThreadLocalを使って、
Thread毎のリソースを管理するパターンです。
Thread毎にリソースを管理するので、同期処理を考えなくて良くなるのが特徴です。
Java
Python
Pythonでは、threading.localを使うことで、
スレッド毎の変数を作ることができます。
12. Active Objectパターン
Threadを使って、RPCを実現するパターンです。
Java
Python
マルチスレッドプログラミングで注意すべきこと
いくつか、マルチスレッドプログラミング特有の
注意すべきことがあるので、まとめておきます。
reentrant lock (再入可能ロック)
eentrant lock (再入可能ロック)は同じオーナーであれば、
何度もロックをとれるロック機構のことです。
yosuke-furukawa.hatenablog.com
Javaでは、java.util.concurrent.locks.ReentrantLockとして標準提供されています。
Pythonでも、threadingモジュール内で、reentrant lockは提供されています。
reentrant lockのことを、再帰ミューテックスということがあります。
また、JavaのSynchronizedブロックも再入可能です。
このreentrant lockは、これはすでにロックを取得したかどうかを
管理するのが難しいときに便利です。
reentrant lockでないロックを使って、複数回ロックを取ると、
デットロックが発生する可能性があります。
但し、reentrant lockを使わなくて良い場合は使わずに、
普通のlockを使うほうが良いようです。
JavaのsynchronizedブロックとLockの実装クラス
synchronizedとLockの実装クラスでは同じようなことを実現できますが、
Lockの実装クラスの方が一般的に激しい競合下での性能が優れているようです。
また Lockの実装クラスでは、ロック取得時にタイムアウトを設定したり割り込みをできるようにしたりなど、
より一層細かい制御を行うことができます。
参考資料
増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編
MyEnigma Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。