【実は別物】並行と並列、プログラミングでの違いを知ろう

concurrency-and-parallelism

並行プログラミング並列プログラミング。なんだか似ているこの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つ。

並列処理と並行処理の違い
  1. 「解決必須」の並行処理「解決手段のひとつ」の並列処理
  2. 並行処理は、並列処理でも実現できる
  3. 並列処理ができないプログラミング言語がある

「解決必須」の並行処理「解決手段のひとつ」の並列処理

先ほどの例を引用しましょう。ネットワークからのダウンロード処理を考えると、

  • データのダウンロードを行う
  • ダウンロード中に、「〇〇%完了」のような進捗表示をする

これがプログラム仕様にあるなら、「ダウンロード」「進捗表示」の2つの並行処理を、必ず実現する必要があります。並行処理は、必ず解決しなければいけない問題なのです。


違う例を考えます。

  • 大量大容量の写真を一度に補正する

こういう処理量が多いものは、単純にプログラミングすると、ものすごい時間がかかります。

これを時間短縮するには、こんな方法があります。

  • CPU性能の高いコンピューターで計算する
  • 処理アルゴリズムを改善する
  • 画像の部分ごとに並列処理で分割する

解決方法はいろいろあります。並列処理は、あくまで数ある解決手段のうちのひとつなのです

並行処理は、並列処理でも実現できる

日常生活での、並行処理を思い出してみましょう。

洗濯の最中に、料理をしよう

並行処理の例ですが、でもこれ、

洗濯をする、パートナーが料理を作る

という、ふたり協力の並列処理でも、実現できるわけです。

つまり、並行処理の実現方法のひとつとして、並列処理がある。というわけです。

この2つの違いは、このような言われ方もします。

問題領域と解決領域
  • 並行処理は、問題領域の話 - 問題そのものの形の話
  • 並列処理は、解決領域の話 - 問題を解決するための手段の話

並列処理ができないプログラミング言語がある

例えば、Pythonは、並列処理がやりにくいプログラミング言語として知られています。

正確には、Python実装のひとつCPythonでは、グローバルインタプリタロック(GIL)の制約により、マルチスレッド処理ができません。マルチプロセス処理なら可能。

JavaScriptも、並列処理ができません

正確には、ブラウザ上で動くJavaScriptの場合。node.jsの場合はworker_threadsがあります。

スクリプト言語系は、並列処理に制約があることは、覚えておきましょう。

まとめ

並行プログラミング、並列プログラミング。現在のプログラミングでは、必ずといっていいほど登場する技術です。

でも頭で考えているだけでは、なかなかピンとこない概念。ぜひサンプルプログラムをたくさん書いて、その感覚を掴みましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次