N予備校の並行処理プログラミングで学んだ
- スレッドとは?
- マルチスレッドとは?
- 並行処理と並列処理の違いとは?
について説明していきます。
並行処理の必要性について
- 普通の言語でアプリを素直に作ると、シングルコアしか使用されない
- パフォーマンス向上のために複数のコアを使う場合は並行処理プログラミングを行う必要がある
並行処理の歴史
- 1つのOSの上で複数のアプリを動かすためにプロセスという概念が生まれた
- しかしコアが複数になるに従って、複数のコアで仕事をさせるためにスレッドという概念ができた
- スレッドは「軽量プロセス」と呼ばれ、最近のOSのほとんどはスレッドを実行スケジュールの単位として扱っている
スレッドとは
スレッドはプロセスよりも小さな実行単位で以下の特徴があります。
- 同時並行的に非同期実行され、別のCPUまたはコアで動作させることができる
- 複数のスレッドは1つのプロセスによって所有され、プロセス内の同じメモリを共有する
- スレッド同士の共有データへのアクセスは明示的な調停が必要
プロセスとスレッドはこのような関係です。プロセス1に対して複数のスレッドが存在可能です。
ちなみにプロセスは固有のメモリを持っています。
自分自身しか参照できなのメモリのため、別プロセスのメモリ情報を直接参照することはできません。
スレッドはプロセスのメモリを参照し、全スレッドが共通のメモリを参照することができます。
そのため共有メモリをうまく扱う必要がある点が、複数のスレッドを使うマルチスレッドなアプリケーション作りのネックになります。
並行処理と並列処理
混同してしまいがちですが、並行(Concurrent/コンカレント)と並列(Parallel/パラレル)は明確に違います。
まずはこれを理解しましょう。
もともとのパソコンは1つのCPUで動いていました。しかしネットサーフィンをしながら、ワードを開いて編集もしたいとなると同時に複数のプロセスが制御できなければなりません。
そこでCPUはコンテキストスイッチとよばれる機能を使い、人に意識させることなく今処理すべきプロセスを瞬時に切り替えることで「ネットサーフィンをしながら、ワードを開いて編集もする」を実現してきました。これを並行処理といいます。
一方で並列処理とは、CPUやパソコンを並べて完全に同時に実行できる処理のことをさしています。
先ほどの図でも示した通り、全ての処理が同じタイミングに走っています。
ややこしいのはマルチスレッドをつかったプログラミングの場合、1コアで動かす場合は並行処理ですが、複数のコアでそれぞれのスレッドを同時に動かすのであれば並列処理ということになります。
並行処理プログラミングが利用される場面
マルチスレッドを用いた並行処理は複雑なためパフォーマンスが必要な領域以外では利用されにくいです。
実際に利用される場面としては以下があります。
- スループットが必要なミドルウェアやサーバ
- レスポンスが良いゲームやデスクトップ・スマホアプリ
1ではWebサーバやAPIサーバがこれにあたり、CDNなどによってキャッシュが十分に効いている場合はバックエンドの処理やHTMLへのレンダリング処理がCPUボトルネックになることがあるので並行プログラミングを使うことがあります。
2では画面描画の処理をしている裏で、スコア判定やゲーム自体を進行させる処理などを並行で行う必要がありますね。
マルチスレッドで生じる問題
マルチスレッドの場合は複数のスレッドが同じ変数を参照できます。そのため変数をきちんと分けておかないと処理が意図しないものとなります。
以下はサンプルコードとその実行例です。
このコードでは「2つのスレッドを起動し、counter変数を+1づつしていく処理」を5回づつまわしました。
本来であれば
$ python3 thread.py <Thread(Thread-1, started 123145309511680)> : 1 <Thread(Thread-1, started 123145309511680)> : 2 <Thread(Thread-2, started 123145314766848)> : 1 <Thread(Thread-1, started 123145309511680)> : 3 <Thread(Thread-2, started 123145314766848)> : 2 <Thread(Thread-1, started 123145309511680)> : 4 <Thread(Thread-2, started 123145314766848)> : 3 <Thread(Thread-1, started 123145309511680)> : 5 <Thread(Thread-2, started 123145314766848)> : 4 <Thread(Thread-2, started 123145314766848)> : 5
となるはずですが、変数を各スレッド間で共通化しているため最大値が10となっています。
$ python3 thread.py <Thread(Thread-1, started 123145309511680)> : 1 <Thread(Thread-1, started 123145309511680)> : 2 <Thread(Thread-2, started 123145314766848)> : 3 <Thread(Thread-1, started 123145309511680)> : 4 <Thread(Thread-2, started 123145314766848)> : 5 <Thread(Thread-1, started 123145309511680)> : 6 <Thread(Thread-2, started 123145314766848)> : 7 <Thread(Thread-1, started 123145309511680)> : 8 <Thread(Thread-2, started 123145314766848)> : 9 <Thread(Thread-2, started 123145314766848)> : 10
コードはこちら
import threading counter = 0 # # スレッドによって呼び出される処理 # counterを加算していくだけ # def work(): # スレッド間で共有する変数をglobalから持ってくる # 参考 : https://www.it-swarm.dev/ja/python/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%A7%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B/1043441666/ global counter for i in range(5): counter = counter + 1 print("{} : {}".format(threading.current_thread(),counter)) if __name__ == '__main__': # rangeで指定した数だけスレッドを生成する for i in range(2): threading.Thread(target=work).start()
この他にもマルチスレッドには
- 2つのスレッドがお互いの処理の完了を待ちあってしまう「デッドロック」
- 特定のスレッドだけが実行される「飢餓状態」
- 特定のスレッドがなんども失敗し全体の処理が固まる「ライブロック」
- スレッドの切り替えによるコンテキストスイッチで処理のコストが高まる
などの問題が存在します。
マルチスレッドの問題の根源は「変数の状態」です。複数のスレッドがこの値をバラバラに制御・操作するため様々な問題を引き起こしています。
マルチスレッドの問題を回避する方法
- 変数を複数のスレッドが共有しないようにする
- 変数を不変にする(変更させない)
- 変数へのアクセスを常に同期する
これらのいずれかを使い対処を行なった変数(クラスやオブジェクト)のことを「スレッドセーフ」と言います。
問題を解いてみる
スレッド 1 は、3000 ミリ秒待った後、現在の UNIX 時刻のミリ秒を取得してフィールド now に代入し、 スレッド 2は、now に値が新たに代入されるまで待ち続け、コンソールに出力するようにしてみてください。
import threading import time now = None def getTime(): # 3000ミリ秒待つ time.sleep(3) # 現在時刻を取得 global now now = time.time() def printTime(): global now print(now) if __name__ == '__main__': #スレッド1 threading.Thread(target=getTime).start() while True: # nowに値が代入されたら if now != None: #スレッド2 threading.Thread(target=printTime).start() break