並行プログラミングと並列プログラミング。なんだか似ているこの2つ。
簡単に言えば、どちらも「複数のコードを同時に実行すること」です。じゃあ、こんなこと思ったことありませんか?
並行プログラミングと並列プログラミング、どちらでやるのがいいんだろう…?
実は、この問いかけ自体が的外れ。並行と並列、この2つには単純に比較できない、大きな差があるのです。
日常生活でイメージする、並行処理と並列処理
まずは日常生活でイメージしてみます。「なにかとなにかを同時に実行する」シチュエーションですね。
並行処理:洗濯の最中に料理をしよう
洗濯物を洗濯機に入れ、スイッチONで、洗濯を開始。
洗濯機が回っている間は、時間が空きます。この間にお昼ごはんの準備を済ませましょう。
これは、「洗濯」というタスクと、「料理」というタスクを、同時に実行しています。これが、並行処理です。
並列処理:ふたりで片付けを済ませよう
お昼ごはんが終わったら、お片付け。手早く終わらせたいので、ふたりで協力して皿洗いを済ませましょう。
「皿洗い」というタスクを、ふたりで一緒に行う。これが、並列処理です。
時間短縮のアプローチの違い
洗濯の最中に料理をする、ふたりで一緒に片付ける。どちらも目的は、家事を手早く終わらせたい!
でも、その考えかたが異なります。
- 並行処理(洗濯中に料理):洗濯の合い間の時間を使って、料理を作って時間短縮
- 並列処理(ふたりで片付け):ふたりで同時にお皿を洗って、時間短縮
つまり、このような言い方ができます。
- 並行処理とは、空き時間の有効活用
- 並列処理とは、複数の人で同時実行
似たような言葉でも、意味合いは異なりますね。
プログラムでみてみよう
次は、実際のプログラミングで考えます。「複数のコードを同時に実行して、時間短縮する」というものです。
まずは、逐次実行プログラム
週の文字を、日本語と英語でそれぞれ表示する、単純なPythonプログラムです。
# 日本語で週を数える
def count_weeks_japanese():
weeks = ["日", "月", "火", "水", "木", "金", "土"]
for week in weeks:
print(week)
# 英語で週を数える
def count_weeks_english():
weeks = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
for week in weeks:
print(week)
# メイン処理
def main():
# 日本語で週を数える
count_weeks_japanese()
# 英語で週を数える
count_weeks_english()
if __name__ == "__main__":
main()
結果はこうなります。
日
月
火
水
木
金
土
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
なんてことはない、普通のプログラムですね。
並行プログラムを作ってみる
ではこれを、並行プログラムにしてみましょう。日本語と英語を同時に表示します。asyncioというパッケージを使います。
import asyncio
# 日本語で週を数える
async def count_weeks_japanese():
weeks = ["日", "月", "火", "水", "木", "金", "土"]
for week in weeks:
print(week)
await asyncio.sleep(1) # 1秒待機
# 英語で週を数える
async def ccount_weeks_english():
weeks = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
for week in weeks:
print(week)
await asyncio.sleep(1) # 1秒待機
# メイン処理
async def main():
# 「日本語で月を数える」と「英語で月を数える」を並列実行
tasks = [count_weeks_japanese(), ccount_weeks_english()]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
結果はこうなります。
日
Sunday
月
Monday
火
Tuesday
水
Wednesday
木
Thursday
金
Friday
土
Saturday
出力が交互になりました。2つの関数が同時に動いている、ということになります。これが、並行処理です。
次は、並列プログラム
次は並列処理です。今度はJavaでやってみましょう。ExecutorServiceを使います。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParallelExecution {
public static void main(String[] args) {
// スレッドプールの作成
ExecutorService executor = Executors.newFixedThreadPool(2);
// 並列実行するタスクの登録
executor.execute(ParallelExecution::countWeeksJapanese);
executor.execute(ParallelExecution::countWeeksEnglish);
// すべてのタスクの終了を待って、ExecutorServiceを終了
executor.shutdown();
}
// 日本語で週を数える
public static void countWeeksJapanese() {
String[] weeks = {"日", "月", "火", "水", "木", "金", "土"};
for (String week : weeks) {
System.out.println(week);
try {
Thread.sleep(1000); // 1秒待機
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 英語で週を数える
public static void countWeeksEnglish() {
String[] weeks = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
for (String week : weeks) {
System.out.println(week);
try {
Thread.sleep(1000); // 1秒待機
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
結果はこう。
日
Sunday
月
Monday
火
Tuesday
水
Wednesday
Thursday
木
Friday
金
Saturday
土
これも、2つの関数が動いていますね。
おや…?結果が交互になっていないところがあります。
水
Wednesday
Thursday
木
なにが起こっているのでしょう?
プログラムの動きの違い
それぞれのプログラム、どのような動きをしているのか、イメージしてみます。
逐次実行プログラム:順番に実行
普通の逐次実行プログラム。count_week_japaneseが動き、終わったらcount_week_englishが動きます。普通ですね。
並列プログラム:バトンタッチしながら実行
続いて、並行処理。ポイントはawait asyncio.sleepです。
# 日本語で週を数える
async def count_weeks_japanese():
weeks = ["日", "月", "火", "水", "木", "金", "土"]
for week in weeks:
print(week)
await asyncio.sleep(1) # 1秒待機
# 英語で週を数える
async def ccount_weeks_english():
weeks = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
for week in weeks:
print(week)
await asyncio.sleep(1) # 1秒待機
count_weeks_japaneseでひとつprintすると、sleepします。するとcount_weeks_englishが動き出します。
片方がsleepすると、もう片方が動き出す。これを繰り返すことで、バトンタッチしながら動作します。
つまり並行処理は、疑似的に、同時実行しているように見せかけているのです。なので、必ず交互に結果が出力されるのですね。
並列プログラミング:本当に同時実行
一方、Javaの並列プログラミングです。
public static void main(String[] args) {
// スレッドプールの作成
ExecutorService executor = Executors.newFixedThreadPool(2);
// 並列実行するタスクの登録
executor.execute(ParallelExecution::countWeeksJapanese);
executor.execute(ParallelExecution::countWeeksEnglish);
// すべてのタスクの終了を待って、ExecutorServiceを終了
executor.shutdown();
}
executor.executeで、2つの関数をそれぞれ起動します。起動された2つの関数は、本当に同時に動きます。
各々のタイミングで表示しているので、まれに出力順番が入れ替わったりするのですね。
プログラムのタスクを動かす実行単位を、スレッド(Thread)と言います。
このJavaのプログラムは、ExecutorServiceをつかって、各々の関数を2つのスレッドで動作させています。これをマルチスレッドといいます。スレッドはそれぞれ独立に動きます。
一方で、Pythonはシングルスレッドです。ひとつのスレッドがタスクを切り替えながら動くのです。
コンカレンシーとパラレリズム
並行プログラミングと並列プログラミング、似ているこの2つの言葉。英語でいうと、ぜんぜん違う響きになります。
- 並行プログラミング:Concurrent programming(コンカレントプログラミング)
- 並列プログラミング:Parallel programming(パラレルプログラミング)
もうひとつ、並行性/並列性という言葉も覚えておきましょう。
- 並行性:Concurrency(コンカレンシー)
- 並列性:Parallelism(パラレリズム)
英語だと区別つきやすいですね。
どんなシチュエーションで使うのか?
動きが異なる、並行プログラムと並列プログラム。さきほどの記述を使うと、こうなります。
- 並行プログラミングとは、空き時間を有効活用することで、時間短縮をはかる処理
- 並列プログラミングとは、複数処理を同時実行することで、時間短縮をはかる処理
実際には、どんなときに使うのでしょうか?
並行プログラミング:I/Oバウンドタスクに
「空き時間を有効活用」する、並行プログラミング。「空き時間」とは何でしょう?
プログラミングにおける空き時間は、コンピュータ外部との入力/出力処理のときに発生します。
- ファイルの読み取りや書き込み
- ネットワークリクエスト
- ユーザー入力の待機
例えばネットワークリクエスト。応答が返ってくるまで、空き時間が生じますね。
入出力により性能が制限される処理を、I/Oバウンドタスクといいます。Input/OutputでI/Oですね。
I/Oバウンドタスクをこなす合い間に、コンピュータは別のことをしたりします。
ネットワークからのダウンロードを待つあいだ、並行して、あと〇〇%のような表示もしたり。
I/Oバウンドタスクを時間短縮するのが、並行プログラミングです。
並列プログラミング:CPUバウンドタスクに
一方、並列プログラミングは、実際に「複数処理を同時実行」します。
これが有効なのは、膨大な計算をフルに行う処理です。
- 数値計算
- 画像処理
- AI学習
例えば画像処理。縦1,024、横1,024の画像なら、1,024×1,024=1,048,576回という膨大な計算が生じます。
このような、計算能力がダイレクトに性能に影響するような処理を、CPUバウンドタスクといいます。コンピュータの頭脳であるCPUがぶんぶん計算する処理ですね。
CPUバウンドタスク中は、空いてる時間などありません。時間短縮するには、複数を同時に動かす必要があるわけです。
CPUバウンドタスクを時間短縮するのが、並列プログラミングです。
並行処理と並列処理の決定的な違い
似ているようで違う、並行処理と並列処理。ここからは両者の大きな違いを考えます。
決定的な違いは、以下の3つ。
- 解決必須の並行処理、解決手段のひとつの並列処理
- 並行処理は、並列処理でも実現できる
- 並列処理ができないプログラミング言語がある
「解決必須」の並行処理、「解決手段のひとつ」の並列処理
先ほどの例を引用しましょう。ネットワークからのダウンロード処理を考えると、
- データのダウンロードを行う
- ダウンロード中に、「〇〇%完了」のような進捗表示をする
これがプログラム仕様にあるなら、「ダウンロード」「進捗表示」の2つの並行処理を、必ず実現する必要があります。
並行処理は、必ず解決しなければいけない問題なのです。
違う例を考えます。
- 大量にある大容量の写真を一度に補正する
こういう処理量が多いものは、単純にプログラミングすると、ものすごい時間がかかります。
これを時間短縮するには、こんな方法があります。
- CPU性能の高いコンピューターで計算する
- 処理アルゴリズムを改善する
- 画像の部分ごとに並列処理で分割する
そう、ものすごく高性能なマシンを持ってくる手だってあるのです。解決方法はいろいろあります。
並列処理は、あくまで数ある解決手段のうちのひとつなのです。
並行処理は、並列処理でも実現できる
日常生活での、並行処理を思い出してみましょう。
洗濯の最中に、料理をしよう
並行処理の例ですが、でもこれ、
洗濯をする、パートナーが料理を作る
という、ふたり協力の並列処理でも、実現できます。
つまり、並行処理の実現方法のひとつとして、並列処理があるのです。
この2つの違いは、このような言われ方もします。
- 並行処理は、問題領域の話 - 問題そのものの形の話
- 並列処理は、解決領域の話 - 問題を解決するための手段の話
並列処理ができないプログラミング言語がある
例えば、Pythonは、並列処理がやりにくいプログラミング言語として知られています。
JavaScriptも、並列処理ができません。
スクリプト系の言語は、並列処理に制約があることは、覚えておきましょう。
まとめ
並行プログラミング、並列プログラミング。現在のプログラミングでは、必ずといっていいほど登場する技術です。
でも頭で考えているだけでは、なかなかピンとこない概念。ぜひサンプルプログラムをたくさん書いて、その感覚を掴みましょう。
コメント