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

concurrency-and-parallelism

並行プログラミング並列プログラミング。プログラミングで立ちはだかる、鬼門のひとつ。

簡単に言えば、どちらも「複数のコードを同時に実行する」ということ。

じゃあ、こんなこと思ったことありませんか?

新人くん

並行プログラミングと並行プログラミング、どちらでやるのがいいんだろう…?

実は、この問いかけ自体が間違い。並行と並列、この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つ。

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

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

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

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

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


違う例を考えます。大量の写真を補正するような画像処理。特に工夫もせずプログラミングすると、ものすごい時間がかかります。

これを時間短縮するには、以下のような方法があります。

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

計算を素早くする、計算方法を変える、同時に実行する…解決方法はいろいろあります。並列処理は、あくまで数ある解決手段のうちのひとつなのです

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

まとめ

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

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

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

コメント

コメントする

CAPTCHA


目次