🦥

並行実行・並列実行:普通の人間がわかるように解説する。

2024/12/15に公開

ロブです!
今日のレッスン:並列と並行の処理を普通の人がわかるように説明する。レッスンの最後に自分のパソコンで楽しめるようにJavascriptとGolangコードを貼っとく。段階で説明する(並列なし・並列あり・並行ありの順番で)。始めよう。

あなた:並行と並列の違いが複雑ですか?


僕:YES

あなた:じゃあ、やっぱり諦めた方がいいですか?


僕:NO

情報を逆にめっちゃ省くから覚悟しよう。

実は、OSタスクスケジュラー、プログラミング言語ランタイムなどの様々な複雑なものがあって、現実はこれより結構複雑なものですが、それはどうでもいいです。そう言った話に興味があったら、ガッツリ説明してくれる記事は山ほどあるから、この記事を最後まで読んで私を応援してフォローしてから他の記事を読んでもいいですよ(我輩は寛容であるから)。

コアとスレッドの違い(簡単説明)

コアとスレッドの違いを簡単に説明する。言われてもわからない「ちゃんとした」定義:

CPU:パソコンの脳みそ。
コア:CPUの物理的な動くパーツ。
スレッド:コアが使えるプロセスのステートなどを保存して早くコンテキストスイッチできるように設定されているソフトウェアで作られているバーチャル機械(????????)

オタク(たぶん)

人間らしく定義:

CPU:パソコンの脳みそ。高ければ高いほど早く動く。
コア:どうでもいいものです。
スレッド:一つのプログラムを管理して色々計算してくれるもの。

この僕

要するに、複数のプログラムを実行したいなら、複数のスレッドが必要です。さて、始めよう。

スレッド一つ、並列なし

激安徳用のスレッドが一個しかないCPUを手に入れました。こんな感じです:

僕の美しい図にご感動ください

これを使ってJavascript(JS)を書きましょう。JSがわかっている人:すべてのファイルIOなどをAsyncでやってみているのが前提。JSがわかっていない人:上の発言をガン無視してください。全然関係ないです。例えば、僕らがファイルを10回ぐらい書くプログラムを作りたい。ファイル名が1.txt,2.txt,3.txtなど。まずはJSでファイル名を決める(1.txt)。そして、ハードドライブに書く。保存できたら、次のファイル名を決めて(2.txt)ハードドライブに書く。

スレッド一つでやるとこうなる:


実は結構頑張った、これ

JSが早いと言っても、OSが割に遅くて全体的に時間がかかる。改善しよう。

スレッド二つ以上、並列あり

今回はスレッド二つあるCPUを手に入れた!全く同じようなプログラムを実行しよう!そして、今度はJSが一つのスレッド、OSが一つのスレッドで分けていこう!(JSが自動にしてくれるやつ、なにも設定しなくていい)

ギャップが多い

並列処理をようやく手に入れた!。。。あれ?これがどうなっている?遅くない?JSが一つのスレッドから「ファイルを保存したい場合、他のスレッドで保存してもらう。俺がこのまま進む」としている。だが、他のスレッドは一つしかない上、そのスレッドがファイルを保存し終わるまで、JSのスレッドは待つしかない。JSスレッドがどんだけ保存処理を他のスレッドに投げたいと言っても、他のスレッドがない程度は仕方がない。

これは困った。もっと高いCPUを買おう。今回は6スレッドが使えるようになる!

買いすぎに後悔なし!

これがいい!JSがカウントしながら他のスレッドがファイルを保存でき、何倍も早くプログラムが処理できた!この段階でちょっと勝利の余韻を味わってから、考えが出る。JSスレッドでやっているファイル名作業も二つのスレッドに分けて行うとスピードがほぼ倍になるじゃない?そうしましょう!

stampo.funから

なぜかというと、JSのルールは「JSの処理が一つのスレッドに限るのです」。これがJSのリミットで、諦めてもいいという人もいるかも。

JSのリミットは僕らのリミットではない。その限界を越えよう、Golangで。

スレッド二つ以上、並行あり(Golangで)

並行処理と並列処理の違いが一つだけ:並行処理はプログラム自体を複数のスレッドに渡ってで実行できる。Golangに「スレッドを二つ使っていいよ」と言うとこんな感じになる:

フルパワー!

この図がちょっと複雑で要説明だと思う。最初はGolangができるだけGoの処理(ファイル名を決めて保存を頼む)を早くしたいため、二つのスレッドに配ってGolang処理を優先する。だが、他のスレッドがいっぱいになった。この状況ならGolangは空くまで待つのではなく、できるだけ早く処理しようとする。そのため、スレッド2番目をOSでファイル保存のために使う。

つまり、できればGolangの処理をスレッド二つでやろうとするけど、必要に応じてスレッド一つだけを使うケースもある。

ちなみに、Golangのためにスレッドを六つ使っていいよと言うとこうなる:

画像はイメージです

違いがわかってきた時点で綺麗にまとめましょう

綺麗にまとめるとこう言える:並列処理は複数のことを同時にやる。一つのスレッドが他のスレッドに仕事が頼めるが、頼めない仕事もあって一人でやるしかない。並行処理は複数のことを同時にやる上、頼めない仕事はない。どんな処理でも他のスレッドもできる(これが簡単説明版で、実は頼めないこともあるけどそれは今気にしなくていいです)。

セットアップなしで試してみよう

セットアップなしでJSとGolangがインストールされている方、こちらのコピペをお願いします。

index.jsにこれをコピペ:

// おらおら
const count = async (num) => {
	for (let i = 0; i < 10; i++) {
		process.stdout.write("おら")
        for (let i = 0; i < 1e8; i++) {
		}
		_ = i
	}
}

const main = async () => {
    console.time("\ntime to finish");
    const iterations = 7
    for (let i = 0; i < iterations; i++) {
        count(i)
    }
    console.timeEnd("\ntime to finish");
}
main()

node index.jsを実行すると2.298秒かかる

Golangタイム。go.modにこれを貼る:

module parallel

go 1.19

main.goにこれ:

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func count(num int) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Print("おら")
		i := 0
		for ; i < 1e8; i++ {
		}
		_ = i
	}
}

func main() {
	start := time.Now()
	wg.Add(7)
	for i := 0; i < 7; i++ {
		go count(i)
	}
	wg.Wait()
	fmt.Printf("%s: %v\n", "\nTime to finish", time.Since(start))
}

まずスレッド一つを使おう:
GOMAXPROCS=1 go run ./main.go

実装すると:time to finish: 2.261秒。ほんのわずかの違いしかない。

次回は全部のスレッドを使う(僕の場合は8スレッド):

go run ./main.go

実行すると:Time to finish: 484.5ms。 1/4の時間しかかかってない!限界を突破した!

###最後:言語力をアップしよう。

この記事を日本語で書けたのがKichiのおかげです。僕が7年にわたって開発し続けながらほぼ毎日使っている言語勉強アプリ(もちろん英語対応!)。知らない言葉を読んだらブラウザーでも携帯でもちらっと説明してくれて、周りの文章と一緒に賢いフラッシュカードを作って、忘却曲線に従ってレビューできる。便利な機能をめっちゃ搭載しているし、復習も楽しいから、是非使ってください。一週間が無料期間で使い放題。アプリストアからダウンロードできるけど上のリンクを使ってサイトで登録すると初月500円割引+友達が招待できるようになる!

Discussion