概要
- マルチスレッドについて今まであまり意識せずコーディングをしてきたため、理解を深めてみる
- ちょっと仕事でpythonを触る必要がありそうなので、その勉強としてもやってみる
まず、マルチスレッドとは
ある処理を単一のスレッドのみを用いて動作させる環境もしくは手法をシングルスレッドという。対して、複数のスレッドが同時に動作することをマルチスレッドという。 プログラム(概ねプロセス)の開始時にはメインとなるスレッドが動作する。必要に応じてその他の処理をするスレッドを作り、実行させることもできる。 - Wikipediaより
フム、、、なるほど、、😷
そもそも、同期・非同期とシングルスレッド・マルチスレッドはいったい何が違うのだろう?
非常にざっくり違いを考えてみたが、
同期/非同期は「待つ/待たない」という方式の差異のこと
スレッドのシングル/マルチは同時に実行する「処理の流れ」を「単数/複数」作るかどうかということ
つまり、こんな感じ?
- 非同期式のスレッド(シングルスレッド) ← あり得る
- 同期式のスレッド(シングルスレッド) ← 普通
- 非同期式のスレッド(マルチスレッド) ← 普通
- 同期式のスレッド(マルチスレッド) ← 別にできるが、意味ない
Javascriptなどはシングルスレッドで動いているため、実行の準備ができたコールバック関数が待ち行列をつくり、現在実行中の関数の処理が終了してから、順に関数が実行されていくような処理をしている。
マルチスレッド対応している言語はあえてシングルスレッドで非同期処理をする意味がないため、シングルスレッドでやるのは逆にしんどい
マルチスレッドのメリット・デメリット
メリット
- プログラムをスレッドに分割すると、メモリコンテキストを共有しながら並行に実行できる
- マルチコアCPU上でマルチスレッド化すると、各スレッドが別々のCPUに割り当てられ同時に並列して実行することでプログラムの速度が向上する.
- また、プログラムの速度とは別で、UI持つアプリケーションであればUXの向上を図ることが可能
- 時間のかかるタスクをバックグラウンドで処理したり、ユーザに一定時間内にフィードバックを返すようすることが可能
デメリット
- 外部リソースを利用していない場合、シングルコアCPU上では,マルチスレッド化しても高速にならない
- プログラミングミスが起こりやすい
- むしろ遅くなる可能性がある
- マルチスレッドではシングルスレッドに比べて仕事を細分化する処理や同期制御が追加で必要になる。簡単ですぐ終わる仕事をマルチスレッドにしたりすると、これらの処理・制御が追加された分処理時間が増加してしまう必要がある
気をつけるべき点
マルチスレッドであるということは、すなわち同期的であるということになるが、
その場合でも他スレッドと同期を取ったり、資源の競合を避けるようすることがあり得る(メリット・デメリットにも書いたが、複数スレッドで同じ変数を使い回すなど、バグの温床になりがち)
例えば、以下のようなシンプルな例を見てみる。
a = 1
print(a) # 1が表示される
# スレッド分岐処理
# 別スレッドでa = 2のような処理があるとする
print(a) # 1 or 2が表示される
マルチスレッドでは、スレッド分岐前に宣言した変数の中身(メモリ)は分岐後のスレッドで共有される。
つまり、片方のスレッド内でその変数を書き換えた場合、もう片方のスレッドにも影響が出る。
順序が明示的に定められていない場合、本当に必要な情報が変数内に入っているかどうかを保証することができず、プログラムとして非常に危うい代物となってしまう。
変数の中身は書き換わっているかもしれない、ということを念頭に置いてプログラミングする必要がある。
その際に、セマフォ
やミューテックス
と呼ばれる考え方がよく使われる。
- セマフォ ... 異なるスレッド間で共有される変数などの共有資源に対する 同時アクセス数を制限する機構のこと。
- ミューテックス ... 複数スレッドから共有資源へのアクセス相互排他(MUTual EXclusion)制御を実現する機構です。ミューテックスの利用によって、あるタイミングにおいて共有資源へアクセス可能なスレッドがただ1つしか存在しないことを保証する。
スレッドのロックについて
- 前述のように、ある操作とある操作の間に別スレッドの処理が挟まると、意図しない挙動となる可能性がある。
- セマフォ・ミューテックスとは別で、ある一連の処理を不可分(atomic)に処理するためにスレッドの
ロック
という考え方もある - ロックを使うと、readとtruncateは不可分に実行されることが保証される。
from threading import Lock
lock = Lock()
with lock:
with open(path, 'r+') as f:
content = f.read()
f.truncate(0)
注意点
- ロックを取得したスレッドは、それを解放するまでCPUリソースを独占できる。
- しかし、ロックを解放しない限り、CPUがそのスレッドに貼り付け状態になるので注意が必要。
- 複数のスレッド間でお互いにロックの解放待ち状態になり、プログラムがフリーズしてしまうことがある(デッドロック)
実際に書いてみた
- pythonで、mysqlを操作する処理を想定。
- 大量にログデータなどを登録する処理があり、書き込まれているDBの状態を監視する処理があり、一定件数を超えるとログを削除する、というような仕組みを想定
- 同じプロセスでそんな処理するか?という点については、あまり深く考えない
import threading
import time
import MySQLdb
# Aはただひたすらデータを0.1秒ごとにインサートする
def connect_db_A():
connection = MySQLdb.connect(
host='localhost',
user='root',
passwd='password',
db='testdb')
cursor = connection.cursor()
sql = "INSERT INTO user (id, name) VALUES(1, 'test')"
while True:
cursor.execute(sql)
# 実行
connection.commit()
time.sleep(0.5)
# 接続を閉じる
connection.close()
# Bはカラム数を監視するだけ
def connect_db_B():
connection = MySQLdb.connect(
host='localhost',
user='root',
passwd='password',
db='testdb')
cursor = connection.cursor()
sql = "select count(*) from user"
while True:
cursor.execute(sql)
rows = cursor.fetchall()[0][0]
print(rows)
# 実行
connection.commit()
if (rows > 100):
print("over 100 column")
# DBを削除するCのメソッドを別スレッドで実行
threading.Thread(target=connect_db_C).start()
time.sleep(3)
else:
print("under 100 column")
time.sleep(3)
# 接続を閉じる
connection.close()
# Cはレコード件数が100を超えたら一気に100件削除する
def connect_db_C():
connection = MySQLdb.connect(
host='localhost',
user='root',
passwd='password',
db='testdb')
cursor = connection.cursor()
sql = "DELETE FROM user LIMIT 100"
cursor.execute(sql)
# 実行
print("delete.")
connection.commit()
# 接続を閉じる
connection.close()
# DはA,B,Cと関係なく10秒に20件レコードをINSERTする
def connect_db_D():
connection = MySQLdb.connect(
host='localhost',
user='root',
passwd='password',
db='testdb')
cursor = connection.cursor()
sql = "INSERT INTO user (id, name) VALUES(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test'),(1, 'test')"
while True:
print("add 10 records.")
cursor.execute(sql)
# 保存を実行
connection.commit()
time.sleep(10)
# 接続を閉じる
connection.close()
if __name__ == '__main__':
#スレッド開始
threading.Thread(target=connect_db_A).start()
threading.Thread(target=connect_db_B).start()
threading.Thread(target=connect_db_D).start()
当然ながら、こう書き換えたら、BやCの処理はいつまでも実行されない。
if __name__ == '__main__':
#スレッド開始
connect_db_A()
connect_db_B()
connect_db_C()
シングルスレッドでの処理は、ドミノ倒しのように前の処理が終わらないと次の処理が始まらない。
複数の処理を同じ状況下で実行させたい、、高速化させたい、、ということを実現するために、マルチスレッドという考え方があるが、
例えば上記の処理をシングルスレッドでやろうとすると、一つ一つの処理で連結させなければならず、結果として時間がかかってしまう可能性がある。
1. Aの処理 > Bの処理 > レコードが100件以下 > Aの処理 > ...
2. Aの処理 > Bの処理 > レコードが100件以上 > Cの処理 > Aの処理 > ...
※Dの処理はランダムなタイミングでどこかに挟み込ませる必要がある
所感
- 本当はセマフォやミューテックス、ロックももう少しちゃんと使ってサンプルを作りたかったが、あまり時間がなかったのでそこまで書けず...時間があれば追記したい
- マルチスレッドでのプログラミングは非常に便利ではあるものの、直感的ではなく、バグが混入しやすくなる。
- なので、マルチスレッドを使わないで済むなら使わないに越したことはないと感じた(デバッグもかなり大変そう。バグの所在がわかりづらい)