Part I: 並行処理の基礎¶
1.1 はじめに¶
並行処理を理解するためには、まず逐次処理(Sequential Processing)の特徴と限界を知る必要があります。本章では、パスワードクラッキングという計算量の多い問題を題材に、逐次処理がどのように動作し、なぜ並行処理が必要になるのかを 8 つの言語で実装しながら解説します。
パスワードクラッキングとは¶
SHA-256 ハッシュから元のパスワードを逆算することは計算上不可能です。そのため、ブルートフォース(総当たり)で候補を生成し、ハッシュ値を比較するアプローチを取ります。このアルゴリズムは以下の 3 ステップで構成されます:
- 候補生成 — パスワードの全組み合わせを生成する
- ハッシュ計算 — 各候補の SHA-256 ハッシュを計算する
- 照合 — ターゲットのハッシュと一致するか確認する
1.2 共通の本質¶
逐次処理の構造¶
すべての言語で共通する逐次処理の構造は以下のとおりです:
入力: ターゲットハッシュ、探索空間(文字種 × 長さ)
出力: 一致するパスワード(見つからなければ None/null)
for each candidate in 探索空間:
hash = SHA256(candidate)
if hash == ターゲットハッシュ:
return candidate
return None
なぜ逐次処理では不十分か¶
8 桁のパスワード(数字のみ)の場合、探索空間は 1 億通り です。逐次処理では CPU コアを 1 つしか使えないため、4 コア CPU でも 25% の利用率 にとどまります。残りの 75% の計算能力は無駄になります。
これが並行処理を学ぶ動機です。探索空間を分割し、複数のコアで同時に処理すれば、理論上はコア数に比例した高速化が可能になります。
共通の処理フロー¶
| ステップ | 関数名 | 説明 |
|---|---|---|
| 1 | getCombinations / generate |
候補パスワードの生成 |
| 2 | getCryptoHash |
SHA-256 ハッシュの計算 |
| 3 | checkPassword |
ハッシュの照合 |
| 4 | crackPassword |
逐次探索の実行 |
1.3 言語別実装比較¶
候補生成のアプローチ¶
8 言語の実装は、候補生成の方法で大きく 2 つに分かれます:
| アプローチ | 言語 | 探索空間 | 方法 |
|---|---|---|---|
| 数値ゼロパディング | Python, Java, C# | 00000000〜99999999 |
数値をゼロ埋めして文字列化 |
| アルファベット直積 | Scala, F#, Rust, Haskell, Clojure | aa〜zz(26^n 通り) |
文字集合の直積で候補を再帰生成 |
SHA-256 ハッシュ計算¶
関数型ファースト言語¶
Haskell 実装
module Ch02.PasswordCracker (getCryptoHash, crackPassword) where
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString as BSW
import Data.ByteString (ByteString)
getCryptoHash :: String -> String
getCryptoHash password =
let hash = SHA256.hash (BS.pack password)
in concatMap (\w -> let hex = showHex w ""
in if length hex == 1 then '0' : hex else hex)
(BSW.unpack hash)
Clojure 実装
(ns grokking-concurrency.ch02.password-cracker
(:require [buddy.core.hash :as hash]
[buddy.core.codecs :as codecs]))
(defn sha256 [s]
(-> (hash/sha256 s)
(codecs/bytes->hex)))
マルチパラダイム言語¶
Scala 実装
import java.security.MessageDigest
object PasswordCracker:
def getCryptoHash(password: String): String =
val md = MessageDigest.getInstance("SHA-256")
val bytes = md.digest(password.getBytes("UTF-8"))
bytes.map(b => f"$b%02x").mkString
Rust 実装
use sha2::{Sha256, Digest};
pub fn get_crypto_hash(password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let result = hasher.finalize();
format!("{:x}", result)
}
F# 実装
module PasswordCracker =
open System.Security.Cryptography
open System.Text
let getCryptoHash (password: string) : string =
let bytes = Encoding.UTF8.GetBytes(password)
let hash = SHA256.HashData(bytes)
hash
|> Array.map (fun b -> b.ToString("x2"))
|> String.concat ""
OOP + 並行処理ライブラリ言語¶
Java 実装
import java.security.MessageDigest;
import java.util.HexFormat;
public class PasswordCracker {
public static String getCryptoHash(String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
C# 実装
using System.Security.Cryptography;
using System.Text;
public static class PasswordCracker
{
public static string GetCryptoHash(string password)
{
var bytes = Encoding.UTF8.GetBytes(password);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
Python 実装
import hashlib
def get_crypto_hash(password: str) -> str:
return hashlib.sha256(password.encode("utf-8")).hexdigest()
候補生成と探索¶
数値ゼロパディング方式(Python, Java, C#)¶
Python 実装
def get_combinations(*, length: int, min_number: int = 0,
max_number: T.Optional[int] = None) -> T.List[str]:
if max_number is None:
max_number = int("9" * length)
combinations = []
for i in range(min_number, max_number + 1):
str_num = str(i)
zeros = "0" * (length - len(str_num))
combinations.append("".join((zeros, str_num)))
return combinations
Java 実装
public static List<String> getCombinations(int length, int minNumber, int maxNumber) {
List<String> combinations = new ArrayList<>();
String format = "%0" + length + "d";
for (int i = minNumber; i <= maxNumber; i++) {
combinations.add(String.format(format, i));
}
return combinations;
}
C# 実装
public static List<string> GetCombinations(int length, int minNumber, int maxNumber)
{
var combinations = new List<string>();
var format = new string('0', length);
for (var i = minNumber; i <= maxNumber; i++)
{
combinations.Add(i.ToString(format));
}
return combinations;
}
アルファベット直積方式(Scala, F#, Rust, Haskell, Clojure)¶
Scala 実装(末尾再帰 + for-yield)
private val Letters = ('a' to 'z').toList
def getCombinations(length: Int): List[String] =
@tailrec
def generate(current: List[String], remaining: Int): List[String] =
if remaining <= 0 then current
else
val next = for
prefix <- current
letter <- Letters
yield prefix + letter
generate(next, remaining - 1)
generate(List(""), length)
F# 実装(リスト内包表記 + パイプ)
let getCombinations (length: int) : string list =
let letters = [ 'a' .. 'z' ]
let rec generate (current: string list) (remaining: int) =
if remaining <= 0 then current
else
let next =
[ for prefix in current do
for letter in letters do
yield prefix + string letter ]
generate next (remaining - 1)
generate [ "" ] length
Rust 実装(再帰 + clone)
fn crack_recursive(
crypto_hash: &str,
alphabet: &[char],
prefix: String,
remaining: usize,
) -> Option<String> {
if remaining == 0 {
return if get_crypto_hash(&prefix) == crypto_hash {
Some(prefix)
} else {
None
};
}
for &c in alphabet {
let mut candidate = prefix.clone();
candidate.push(c);
if let Some(result) = crack_recursive(crypto_hash, alphabet, candidate, remaining - 1) {
return Some(result);
}
}
None
}
Haskell 実装(foldr + Maybe)
crackRecursive :: String -> String -> String -> Int -> Maybe String
crackRecursive cryptoHash alphabet prefix 0 =
if getCryptoHash prefix == cryptoHash
then Just prefix
else Nothing
crackRecursive cryptoHash alphabet prefix remaining =
foldr tryChar Nothing alphabet
where
tryChar c acc =
case crackRecursive cryptoHash alphabet (prefix ++ [c]) (remaining - 1) of
Just result -> Just result
Nothing -> acc
Clojure 実装(loop/recur + 基数変換)
(def alphabet "abcdefghijklmnopqrstuvwxyz")
(defn generate-password [index length]
(let [base (count alphabet)]
(loop [idx index
acc '()]
(if (= (count acc) length)
(apply str acc)
(recur (quot idx base)
(cons (nth alphabet (mod idx base)) acc))))))
(defn crack-password [target-hash length]
(let [total (long (Math/pow (count alphabet) length))]
(loop [i 0]
(when (< i total)
(let [password (generate-password i length)]
(if (= (sha256 password) target-hash)
password
(recur (inc i))))))))
戻り値の型表現¶
「パスワードが見つからない」ケースの表現方法は、言語の型システムに大きく依存します:
| 言語 | 戻り値型 | 「見つからない」の表現 |
|---|---|---|
| Python | None(暗黙) |
関数が何も返さない |
| Java | String (nullable) |
null を返す |
| C# | string? |
null を返す |
| Scala | Option[String] |
None を返す |
| F# | string option |
None を返す |
| Rust | Option<String> |
None を返す |
| Haskell | Maybe String |
Nothing を返す |
| Clojure | 動的(nil) |
nil を返す |
安全性の階層:
- コンパイル時保証: Rust (
Option<T>), Haskell (Maybe), Scala (Option), F# (option) - パターンマッチを強制し、
null参照エラーを防止 - 型ヒント: Python (
Optional[T]), C# (string?) - IDE とリンターが警告を出すが、ランタイムでは強制されない
- 規約ベース: Java (
null), Clojure (nil) - 呼び出し側の注意に依存
1.4 比較分析¶
ランタイムとハッシュライブラリ¶
| 言語 | ハッシュライブラリ | 依存関係 |
|---|---|---|
| Python | hashlib(標準) |
なし |
| Java | MessageDigest(標準) |
なし |
| C# | SHA256(標準) |
なし |
| Scala | MessageDigest(JVM 標準) |
なし |
| F# | SHA256(.NET 標準) |
なし |
| Rust | sha2 クレート |
外部依存 |
| Haskell | cryptohash-sha256 |
外部依存 |
| Clojure | buddy-core |
外部依存 |
Python, Java, C#, Scala, F# は標準ライブラリだけで SHA-256 を計算できます。一方、Rust, Haskell, Clojure は外部ライブラリに依存しますが、より洗練された API を提供しています。
コードの簡潔さ¶
SHA-256 ハッシュ計算のコード行数を比較すると、言語の表現力の違いが明確になります:
| 言語 | 行数 | 特徴 |
|---|---|---|
| Python | 1 行 | hashlib のワンライナー |
| Clojure | 2 行 | スレッディングマクロ |
| C# | 3 行 | SHA256.HashData() + Convert.ToHexString() |
| Rust | 4 行 | ビルダーパターン |
| Scala | 3 行 | map + mkString |
| F# | 4 行 | パイプ演算子チェーン |
| Java | 5 行 | MessageDigest + 例外処理 |
| Haskell | 4 行 | ByteString 操作 + concatMap |
探索アルゴリズムの設計思想¶
| アプローチ | 言語 | メリット | デメリット |
|---|---|---|---|
| 命令型ループ | Python, Java, C# | 直感的、デバッグしやすい | 並列化の際にインデックス分割が必要 |
| 末尾再帰 | Scala, F# | スタックオーバーフロー防止、FP らしい | 末尾再帰最適化が必要 |
| 再帰 + 早期リターン | Rust, Haskell | 探索木の自然な表現 | スタック消費(深い探索では問題) |
| loop/recur | Clojure | JVM 上でスタックセーフ | Lisp 構文に慣れが必要 |
1.5 実践的な選択指針¶
ユースケース別の推奨¶
プロトタイピング・学習用途:
- Python — 最も簡潔で、標準ライブラリだけで完結
- Clojure — REPL 駆動開発で素早く実験可能
型安全性を重視する場合:
- Rust — コンパイル時にメモリ安全性と型安全性を保証
- Haskell — 純粋関数型で副作用を型レベルで管理
エンタープライズ環境:
- Java / C# — 豊富なエコシステムと標準ライブラリ
- Scala / F# — 既存の JVM/.NET 資産を活用しつつ関数型の利点を享受
並行化への準備度¶
次章(Part II)で探索を並列化する際、各言語の逐次実装がどれだけスムーズに並行版に移行できるかは重要な指標です:
| 言語 | 並行化の容易さ | 理由 |
|---|---|---|
| Rust | 高い | 所有権システムが並行安全性をコンパイル時に保証 |
| Haskell | 高い | 純粋関数は副作用がなく、安全に並列実行可能 |
| Clojure | 高い | 不変データ構造により共有状態の問題が発生しない |
| Scala | 高い | Future や .par で容易に並列化可能 |
| F# | 高い | Async ワークフローで自然に並列化 |
| Java | 中程度 | ExecutorService で並列化可能だが、共有状態の管理は手動 |
| C# | 中程度 | Parallel.ForEach や Task で並列化可能 |
| Python | 低い | GIL の制約により threading では真の並列化ができず、multiprocessing が必要 |
1.6 まとめ¶
言語横断的な学び¶
- 逐次処理の限界は普遍的 — どの言語で書いても、逐次処理では 1 コアしか活用できない
- 候補生成の設計が並行化を左右する — 探索空間の分割しやすさが重要
- 型安全な戻り値は保守性を高める —
Option/Maybeを使う言語はnull参照エラーを防止 - 言語の思想が実装に現れる — 命令型ループ vs 再帰 vs パイプラインの違い
次のステップ¶
Part II: プロセスとスレッド では、この逐次処理をマルチスレッドで並列化し、CPU の全コアを活用する方法を学びます。
各言語の個別記事¶
| 言語 | 個別記事 |
|---|---|
| Python | Part I - 並行処理の基礎 |
| Java | Part I - 並行処理の基礎 |
| C# | Part I - 並行処理の基礎 |
| Scala | Part I - 逐次処理 |
| F# | Part I - 逐次処理 |
| Rust | Part I - 並行処理の基礎 |
| Haskell | Part I - 並行処理の基礎 |
| Clojure | Part I - 逐次処理 |