CPU 環境の変化
C10K 問題 or C10M 問題
並列
CPU1
CPU2
並行(並列でない)
CPU1
並行(かつ並列)
CPU1
CPU2
処理の種類によって並列化での性能向上が見込めるかは違う
他の処理の完了を待って次の処理に移るかどうかの違い
同期
非同期
イベント
イベントに対応する処理
Blocking
並列化によるスピードアップの法則
並列化によるスピードアップの法則
どう解釈するか?
Gustafson の方が見栄えはいいが、常には使えない
ここまではだいたい用語の話
CPU1
CPU2
タスク
A
タスク
B
タスク
C
タスク
A
タスク
B
タスク
D
タスク
C
タスク
D
複数のタスクに分解して
CPU に振り分ける
CPU1
CPU2
データ
A
処理対象のデータが分割できる場合
データ
B
データ
A
データ
B
#pragma omp parallel for private(j,k)
for (i = 0; i < n; i++){
for (j = 0; j < n; j++){
C[i][j] = 0.0;
for (k = 0; k < n; k++){
C[i][j] += A[i][k] * B[k][j];
}
}
}
逐次処理で正確にコードを書くのは難しい
並列処理の場合はさらに難しくなる
スレッド1
スレッド2
f()
f()
int f() {
(*a)++;
};
スレッド1
スレッド2
load a
store a+1
load a
store a+1
スレッド1
スレッド2
01 load a
02 store a+1
03
04
01
02
03 load a
04 store a+1
case 1
01 load a
02
03
04 store a+1
01
02 load a
03 store a+1
04
case 2
atomic_int tmp = ATOMIC_VAR_INIT (0);
void bad_swap(atomic_int x, atomic_int y) {
atomic_store(&tmp, atomic_load(&x));
atomic_store(&x, atomic_load(&y));
atomic_store(&y, atomic_load(&tmp));
}
swap(*x=3, *y=4)
swap(*u=5, *v=6)
01 tmp = x = 3
02 x = y = 4
03
04
05
06 y = tmp
01
02
03 tmp = u = 5
04 u = v = 6
05 v = tmp
06
*x=4, *y=6
*u=6, *v=5
int x[100000], y[100000];
x[4] = y[4];
x[99999] = y[99999];
x[5] = y [5];
x[99998] = y[99998];
x[4] = y[4];
x[99999] = y[99999];
x[5] = y[5];
x[99998] = y[99998];
x[4] = y[4];
x[5] = y[5];
x[99999] = y[99999];
x[99998] = y[99998];
int * ready;
void thread1() {
*ready = 0;
for (;;) {
if (*ready == 1) {
printf("start!\n");
break;
}
}
}
void thread2() {
sleep(10);
*ready = 1;
}
thread1 と thread2 を同時に走らせる
thread1 は停止するか?→ 停止しない
load と store 命令にどのような並び替えが予想されるか
See https://preshing.com/20120930/weak-vs-strong-memory-models/
pthread_mutex_t swap_lock;
void swap(int * x, int * y) {
int tmp;
pthread_mutex_lock(&swap_lock);
tmp = *x;
*x = *y;
*y = *tmp;
pthread_mutex_unlock(&swap_lock);
}
相互排他をおこなう最も単純なロック
相互排他では効率的でない場合がある
読み込み回数 >> 書き込み回数
このような場合には Reader Writer Lock を使う
var mu sync.RWLock
func read() {
mu.RLock()
defer mu.RUnlock()
println("read")
}
func write() {
mu.Lock()
defer mu.Unlock()
println("write")
}
複数のスレッドがお互いの処理を待ち合っている状態
func thread1() {
m1.Lock()
defer m1.Unlock()
time.Sleep(1 * time.Second)
m2.Lock()
defer m2.Unlock()
}
func thread2() {
m2.Lock()
defer m2.Unlock()
time.Sleep(1 * time.Second)
m1.Lock()
defer m1.Unlock()
}
#include <pthread.h>
void thread_func(void * arg) {
int v = *(int*)arg;
printf("v is %d\n", v);
}
void some_func() {
pthread_t pthread;
int arg = 1;
pthread_create(&pthread, NULL, &thread_func, (void*)&arg);
pthread_join(pthread, NULL);
}
class MyTask implements Runnable {
int _v;
MyTask(int v) {
_v = v;
}
public void run() {
System.stdout.println("value is " + v);
}
}
MyTask t = new MyTask(42);
new Thread(t).start();
t.join();
これを並行処理する
並行処理にはキャンセルやエラーハンドリングがつきもの
ロックはこういった機能を提供してくれるわけではない
みんなロックを使いたくない
なお
スレッド間でメッセージのやりとりを行う
スレッド1
スレッド2
func thread(ch chan bool) {
for {
fmt.Print("waiting")
<-ch
fmt.Print("start!")
}
}
func main() {
ch := make(chan bool)
go thread(ch)
ch <- true
ch <- true
}
(おまけ)
大まかに 2 パターン
性能を出すためには 1 を進めたいが現実は 2 が多い
負荷の種類に対して適切な設計をする必要がある
並行処理特有のバグはタイミングの問題で起こる
デッドロックetcが起きるかどうか静的に解析できる
アルゴリズム
データ構造
ネットワーク