手分けのしかたは二通り
「マルチスレッドで速くする」とは、つまるところ同時に動ける複数のスレッドが仕事のかたまりを手分けして処理することで速くしてるわけですよね。このとき「手分けのしかた」は大きく2つに分類できます。
1つは庭の草むしり。1人でやるのはしんどいけれど、庭をN等分してN人が一斉に草をむしれば1人あたりのノルマは1/Nとなり、スピードはざっくりN倍になります。アレとコレをやんなきゃいけないけれど、アレとコレが独立してるなら2つのスレッドで同時にやれば早く片付きます。
大量の要素が並んだ配列の総和を求める(あんまり面白くない)サンプルを書いてみました。要素数Nの配列:vector<double> data(N)の総和を求めるのに2つのスレッドを起こし、それぞれ配列の前半部と後半部の和を求めます。両者が処理を完了したら、得られた2つの和を足せば総和が求まります。
#include <iostream>
#include <random>
#include <thread>
#include <future>
#include <numeric>
#include <vector>
#include <chrono>
int main() {
using namespace std;
using namespace std::chrono;
const int N = 10000000;
vector<double> data(N);
mt19937 gen; // メルセンヌ・ツイスター
normal_distribution<double> dist; // 正規分布(平均:0,標準偏差:1)
auto rand = [&]() { return dist(gen); };
generate_n(begin(data), N, rand);
double sum;
long long duration;
high_resolution_clock::time_point start;
high_resolution_clock::time_point stop;
// single-thread
start = high_resolution_clock::now();
sum = accumulate(begin(data), end(data), 0.0);
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start).count();
cout << "single thread: " << sum << " in " << duration << "[us]\n";
// 2-threads
start = high_resolution_clock::now();
// forkして
future<double> f0 = async([&]() { return accumulate(begin(data), begin(data)+N/2, 0.0); });
future<double> f1 = async([&]() { return accumulate(begin(data)+N/2, begin(data)+N , 0.0); });
// joinする
sum = f0.get() + f1.get();
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start).count();
cout << " dual thread: " << sum << " in " << duration << "[us]\n";
}
2倍とまではいかないけれど、まぁいいセンいってますかね。
もう一つはラーメン屋。とあるラーメン屋に電話で注文が入ってきます。電話が鳴るたびに注文を受け(入力)、ラーメンこしらえて(処理)、バイク飛ばして届けます(出力)。それぞれに5分かかるとすると、店主1人で切り盛りする"ワンオペ"だったら入力から出力まで15分、次の注文に応じられるのは15分後です。
バイトを雇って3人で店を回すなら、3人がそれぞれ入力/処理/出力を担当し、入力から処理へ/処理から出力へと仕事が流れていきます。お客様からすれば電話してから15分でラーメンが届くことに変わりはないけれど、注文を受けてから5分後には次の注文が受けられるんだから処理能力は3倍です。
condition_variableはラーメン屋の流れ作業を組み立てるのに不可欠なんです。マクラが長くてごめんなさいね。
