並行プログラミングと並列プログラミング。プログラミングで立ちはだかる、鬼門のひとつ。
簡単に言えば、どちらも「複数のコードを同時に実行する」ということ。
じゃあ、こんなこと思ったことありませんか?

並行プログラミングと並行プログラミング、どちらでやるのがいいんだろう…?
実は、この問いかけ自体が間違い。並行と並列、この2つには単純に比較できない、大きな違いがあるのです。
日常生活でイメージする、並行処理と並列処理
まずは、プログラミングの前に、日常生活でイメージしてみましょう。「なにかとなにかを同時に実行する」イメージですね。
並行処理:洗濯の最中に料理をしよう
洗濯物を洗濯機に入れ、スイッチONで洗濯を開始。洗濯機が回っている間は、時間が空きます。この間にお昼ごはんの準備を済ませましょう。
これは、「洗濯」というタスクと、「料理」というタスクを、同時に実行しています。これが、並行処理です。
並列処理:ふたりで掃除を済ませよう
お昼ごはんが終わったら。家の中を掃除しましょう。手早く終わらせたいので、パートナーとふたりで協力して、掃除をささっと済ませましょう。
「掃除」というタスクを、ふたりで一緒に行う。これが、並列処理です。
時間短縮のアプローチの違い
洗濯の最中に料理をする、ふたりで掃除する。どちらも目的は、家事を手早く終わらせたい!
並行処理と並列処理の目的は、どちらも時間を短縮したいということ。でもその考えかたが異なります。
- 並行処理(洗濯中に料理):洗濯の合い間の時間を使って、料理を作って時間短縮
- 並列処理(ふたりで掃除):ふたりで同時に掃除して、時間短縮
まとめると、
- 並行処理とは、空き時間を有効活用することで、時間短縮をはかる処理
- 並列処理とは、複数の人で同時実行することで、時間短縮をはかる処理
似たような言葉でも、意味合いは異なりますね。
言葉のおさらい
並行と並列。言葉だけ見れば同じようですが、プログラミングの世界では、この2つは明確に区別されます。
並行と並列の言葉の意味を、おさらいしておきましょう
コンカレンシーとパラレリズム
似ているこの2つの言葉。英語でいうと、ぜんぜん違う響きになります。
- 並行プログラミング:Concurrent programming(コンカレントプログラミング)
- 並列プログラミング:Parallel programming(パラレルプログラミング)
もうひとつ、並行性/並列性という言葉も覚えておきましょう。
- 並行性:Concurrency(コンカレンシー)
- 並列性:Parallelism(パラレリズム)
この2つの言葉が、対比して使われます。英語だと区別つきやすいですね。
並行性=同時実行性、並列性=平行
並行性(Concurrency)は、同時実行性という言い方もします。同時に起こるという意味を持っています。
並列性(Parallelism)。パラレルターン、パラレルワールドなどのパラレルですね。並んで行く、という意味あいです。
プログラムでみてみよう
プログラミングで考えると、「複数のコードを同時に実行して、時間短縮する」となります。
複数のコードを同時実行、ってどういうことでしょう?実際のコードで、動作イメージを確認しましょう。
まずは、逐次実行プログラム
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でやってみましょう。
やることは、Pythonのときと同じです。
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
木
なにが起こっているのでしょう?
プログラムの動きの違い
Pythonで作った並行プログラムを見てみましょう。ポイントは、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が動く
- sleepで1秒待機。ここで、眠っている間に他の処理にバトンタッチ
- ccount_weeks_englishが動く
- sleepで1秒待機。ここでまた他の処理にバトンタッチ
- count_weeks_japaneseのsleepを抜けたあとから、処理が再開
- そしてまた、sleepしてバトンタッチ→他方の処理が開始→sleep…
処理がsleepしている間に、ほかの処理が動き出す。2つの関数は、ほんとうに同時に動いているわけでなく、疑似的に、同時に動いているように見せかけているのです。
なので、必ず交互に結果が出力されるのですね。
一方、Javaの並列プログラミング。
// 日本語で週を数える
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();
}
}
}
これは、2つの関数が本当に同時に動いています。sleepのところでバトンタッチしているわけではなく、ただ自分が休んでいるだけです。
それぞれが全然別のタイミングで動いているので、まれに出力順番が入れ替わったりするのですね。
プログラムのタスクを動かす実行単位を、スレッド(Thread)と言います。
このJavaのプログラムは、スレッドを2つ使っています。それぞれのスレッドは独立に動きます。これをマルチスレッドといいます。
一方で、Pythonはシングルスレッドです。ひとつのスレッドがタスクを切り替えながら動いています。
どんなシチュエーションで使うのか?
動きが異なる、並行プログラムと並列プログラム。さきほどの記述を使うと、こうなります。
- 並行プログラミングとは、空き時間を有効活用することで、時間短縮をはかる処理
- 並列プログラミングとは、複数処理を同時実行することで、時間短縮をはかる処理
実際には、どんなときに使うのでしょうか?
並行プログラミング: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も、並列処理ができません。
スクリプト言語系は、並列処理に制約があることは、覚えておきましょう。
まとめ
並行プログラミング、並列プログラミング。現在のプログラミングでは、必ずといっていいほど登場する技術です。
でも頭で考えているだけでは、なかなかピンとこない概念。ぜひサンプルプログラムをたくさん書いて、その感覚を掴みましょう。
コメント