Concurrent 同期入門
株式会社ピクセラ
製品事業本部
ソフトウェア開発部門
先端技術開発部
© 2017 PIXELA CORPORATION|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
2019年6月21日
発表の目的
○ マルチスレッドプログラミングにおける競合について知ってもらう
○ 同期オブジェクトについて知ってもらう
○ マルチスレッド設計/実装のノウハウを知ってもらう
2Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
アジェンダ
○ 1: マルチスレッドクイズ
○ 2: 競合とその原因と対策
○ 3: 同期オブジェクト
○ 4: 設計/実装上の注意
○ 5: Atomic 変数操作
3Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
1:マルチスレッドクイズ
4Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
このコードの x がとりうる値は?
5
int x = 0;
void func1() {
for (int i = 0; i < 1000000; i++) x++;
}
int main()
{
std::thread t1([]() { func1(); }), t2([]() { func1(); });
t1.join(); t2.join();
printf("x:%dn", x);
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
2:競合とその原因と対策
6Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
競合 (1) : データ競合 (data race)
○ 複数スレッドで同じ変数に対して同時に Read/Write が発生する
○ 厳密には少なくとも一つが Write となる
○ 発生現象
○ 何が発生するか予想できない
○変更途中の値が Read (変数がレジスタ幅を超える場合)
○Write 順が他のスレッドの Read に反映されない
○ 原因
○コードと CPU 命令の乖離
○CPU の内部動作 (キャッシュ)
7Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
競合 (1) : データ競合 (data race)
8
int my_account = 0; // 私の預金口座残高
int your_account = 100; // あなたの預金口座残高
// 送金処理
bool racy_transfer(int m) {
if (m <= my_account) { // 未定義動作の可能性あり!
my_account -= m; // 未定義動作の可能性あり!
your_account += m; // 未定義動作の可能性あり!
return true;
}
return false;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
競合 (2) : 競合状態 (race condition)
○ 実行タイミングに依存してシステムの出力結果が変化する
○ 発生現象
○ 変数が不変条件に違反した値になる
○ タイミングによって不具合発生、sleep を入れるとクラッシュ
○ 原因
○ 設計/実装ミス
○複数のリソースに対する操作が排他されていない
○スレッド間の調停を行っていない
9Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
競合 (2) : 競合状態 (race condition)
10
std::atomic<int> my_account = 0; // 私の預金口座残高
std::atomic<int> your_account = 100; // あなたの預金口座残高
// 送金処理
bool unsafe_transfer(int m)
{
if (m <= my_account) {
// ★この時点でも(m <= my_account)は真?
my_account -= m;
your_account += m;
return true;
}
return false;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
競合への対策 (基本)
○ 操作を不可分 (atomic) とする
○ atomic とは一連の操作が他スレッドから一体と見えること
○全操作が完了するまで他スレッドから変更が見えない
○操作失敗時には操作前の状態に戻る
○複数スレッドで同時に操作できない
○ 例: 複数スレッドからアクセスされるリソースを排他する
○ 前提条件を満たしてから実行する
○ 他のスレッドの入力を待つような場合
11Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
正しいコード
12
int my_account = 0; // 私の預金口座残高
int your_account = 100; // あなたの預金口座残高
std::mutex txn_guard;
// 送金処理: 安全なトランザクション処理
bool safe_transfer(int m)
{
std::lock_guard<std::mutex> lk(txn_guard);
if (m <= my_account) {
my_account -= m;
your_account += m;
return true;
}
return false;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
3:同期オブジェクト
13Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
代表的な同期オブジェクト
○ Mutex
○ Read-Write Lock
○ Condition Variable
○ Event
○ Semaphore
○ Volatile (正確には同期オブジェクトではない)
14Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Mutex
○ ロックすることでそれ以降のコードへのアクセス権を取得する
○ 同時にロックできるのは一つのスレッドのみ
○ 同じ Mutex をロックしているコード間は排他されている
○ Mutex の作り方によって同じスレッドが複数回ロックをとれる (Recursive) なもの
とできないものがある
○ パフォーマンスが要求されない限り Recursive にするのが安全
○後の修正で Recursive が必要になるかもという理由
15Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Mutex コード例 (C++)
16
int func()
{
static std::recursive_mutex mutex;
std::lock_guard<std::recursive_mutex>
lock(mutex);
static int count = 0;
count++;
return count;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Read-Write Lock
○ Read は複数あっても race condition は起きないことを利用してパフォーマンス向
上を行う
○ Read のロック API と Write API のロックが分かれている
○ Read だけなら複数取得可
○ Write と Read, 複数の Write は同時に行えない
○ Write に比べて Read が多い場合にロック待ち時間を削減できる
17Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Read-Write Lock コード例 (Win32 API)
18
void init(SRWLOCK* lock) {
InitializeSRWLock(lock);
}
static int count = 0;
int read(SRWLOCK* lock) {
// AcquireSRWLockExclusive がない限り通る
AcquireSRWLockShared(lock);
int ret = count;
ReleaseSRWLockShared(lock);
return ret;
}
int write(int val, SRWLOCK* lock) {
// 他の AcquireSRWLockExclusive, AcquireSRWLockShared がない限り通る
AcquireSRWLockExclusive(lock);
count = val;
ReleaseSRWLockExclusive(lock);
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Condition Variable
○ 待機側が待機し、通知側が待機側を起動させる
○ 待機側が待機前にロックオブジェクト (Mutex, Read-Write Lock) を握っているの
が前提となる
○ 待機と不可分に行える
○ “条件変数” とよばれているのは以下のような使われ方のため
○ 待機側は処理条件を満たしていない場合に待機する
○ 通知側は待機側の処理条件を満たした状態にして通知する
19Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Condition Variable コード例 (C++)
20
static std::mutex mutex;
static std::condition_variable cond;
bool flag = false;
void wait() {
// flag が true になるまで待機。
std::unique_lock<std::mutex> lock(mutex);
cond.wait(lock, []() { return flag; });
flag = false;
}
void wake() {
// 待機スレッドを起こす
{ std::unique_lock<std::mutex> lock(mutex); flag = true; }
cond.notify_one();
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Event
○ 通知側が通知を行うと待機しているスレッドが起動する
○ オブジェクトの Signal (通ってよし) と Non-Signal (通行止め) を管理する
○ Signal 状態でロックとると non-Signal になる/ならないとか、初期状態セットでき
るとかバリエーションがいっぱいある
○ ので使う際には OS のリファレンスをちゃんと読む必要がある
○ 典型的な例は I/O 待ち
21Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Event コード例 (Win32 API)
22
int init(HANDLE *handle) {
// 自動で non-signal になる初期値 non-signal のイベント作成
*handle = CreateEvent(/*atrtributes*/NULL,/*manual_reset*/FALSE,
/*initial_state*/FALSE, /*name*/NULL );
}
void wait(HANDLE handle) {
// signal 状態まで待つ
// 自動で non-signal になるので一度に起動するスレッドはひとつ
WaitForSingleObject(handle, INFINITE);
}
void wake(HANDLE handle) {
SetEvent(handle);
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Semaphore
○ 指定回数だけ取得できる
○ 解放は取得者でなくてもよい
○ リソースの共有数の管理に使えるはず
○ Mutex と条件変数でも実装できるが…
23Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Semaphore コード例 (Win32 API)
24
int func()
{
static HANDLE sem = CreateSemaphore(
/*attributes*/NULL, /*initial count*/5, /*max count*/ 12, /*name*/ NULL
);
WaitForSingleObject(sem, INFINITE); // count--, count > 0 でロックされない
// 処理
// count 増加 -> 待機スレッド起動
ReleaseSemaphore(sem, /*count*/1, /*[out] prev_count*/NULL);
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Volatile
○ コンパイラによる最適化を防ぐ
○ 変数アクセス時に逐次メモリを読みに行く
○ループ中のスレッド外部での変数の変更の検出に使える
○ 関数内での命令リオーダーは防げない
○コードの記述順に Read/Write されると期待すべきではな
い
○ Volatile ではなく適切な atomic 機構に頼るべき
○ Volatile を使うケースは以下
○ 外部ハードウェアデバイスを参照している場合
25Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Volatile コード例 (C++)
26
// デバイス側で変更されるフラグ
extern volatile int flag;
int wait() {
// busy-loop でチェック
// 外部で flag が true になれば抜ける
while (!flag) {};
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
注意点
○ 各同期オブジェクトが使用できる/できないは OS や言語によって違う
○ 各同期オブジェクトがプロセス間で共有できる/できないは OS 依存
○ C++ の各オブジェクト操作が OS のどの API に相当するかは stdlib の実装依存となっ
ている
○ パフォーマンスが必要なら軽い API に切り替える必要がある
○ スペースの都合上ヘッダの include は省略した
○ 共有オブジェクトが static 変数になっているのはスペースの都合上なので実装ではやら
ないこと
27Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
4:設計/実装上の注意
28Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
生のロックオブジェクトは扱わない
○ ロックオブジェクトを操作するコードを直接書かない
○ アンロックするコードをロック区間出口の数だけ書く必要性
○ C++ なら std::lock_guard 等を使う
○ RAII (Resource Acquisition Is Initialization)
○ スタック上にのったオブジェクトの破棄時にリソース破棄が発生
○ C の場合 RAII は使えないので goto パターンでアンロックを一か所に集めること
29Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
条件変数の spurious wakeup
○ 条件変数での待機処理にて通知なしでブロック解除される現象
○ 原因はライブラリ内部処理やハードウェア/OS の都合
○ 発生頻度はごく低い…はず
○ 対応 : 条件変数を使う場合は必ず待機終了後に待機条件をチェック
○ 満たしていない場合は再度待機
30Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
デッドロックを防ぐために – 原因
○ デッドロックは 2 個以上のロックの取得順番が違うときに発生する
○ 特に危ないケース
○ ロックを取得したまま他モジュールを呼びに行き、そのモジュールから自モ
ジュールを呼び出す経路がある場合
○同じスレッドでの呼び出しは問題ないが、異なるスレッドで
の呼び出し経路があると最悪
○ モジュール内に複数ロックオブジェクトがある場合
○常にロック順序を一定にする必要がある
31Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
デッドロックを防ぐために – 状況
32Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
デッドロックを防ぐために – 対策
○ 排他区間を短くする
○ モジュール内のロックオブジェクトを一つにする
○ モジュール相互の呼び出しが発生するパスを作らない
○ ロックを取得したまま他モジュールを呼ばない
○ 上位モジュールの処理は上位モジュールのスレッドで処理する
○ いったん上位モジュールのタスクキューで受ける等
○タスクキューが詰まってデッドロックになる場合も (実例)
○ 下位モジュールのロックを処理から切り離す
33Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Sleep 時間はベストエフォート
○ 各種 sleep/待機関数に渡す時間はその時間での起動を保証しない
○ スレッドスケジューリング (スレッド優先度等) の都合上起動が待たされる可能性が
ある
○ また、起動の精度はタイマー割り込みの頻度に依存する
○ Windows: 16 millisecond (デフォルト状態)
○ PC Linux: 4 millisecond (2010 の情報)
34Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
精密な Sleep 時間が必要な場合
○ 精密に起動できるようにする
○ タイマー割り込み頻度の変更 (1 msec 精度まで)
○ スレッド優先度の引き上げ
○ Busy loop の採用
35Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
5:Atomic 変数操作
36Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
ロックによるオーバヘッド
○ 頻繁にアクセスする変数にロックをかけるとパフォーマンス低下
○ 頻繁な状態チェック
○ API 呼び出し -> カーネル空間への移動
○ メモリを Atomic に操作する変数 (正確にはメモリを Atomic に操作する CPU 命
令) を使えばパフォーマンスが向上する
37Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Atomic 変数の前に – data race の原因
○ コードと CPU 命令の乖離
○ CPU 内部の都合
○ キャッシュの同期が基本行われない
38Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Data race : コードと CPU 命令の乖離 (1)
○ 下の処理中でコンテキストスイッチとなった場合、
再開時は昔のメモリを読み込んだ状態で処理再開
される
39
int func(int *ptr) {
*ptr++;
}
int func(int *ptr) {
int val = *ptr;
val++;
*ptr = val;
}
おおよそ
次のように展開される
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Data race : コードと CPU 命令の乖離 (2)
○ 変数がレジスタ幅に収まらない場合、メモリ書き込
み途中で他スレッドから読み取られる可能性がある
○ 矛盾した状態を読み込んでしまう
40
struct St {
int64_t a;
int64_t b;
};
int set(St *ptr) {
*ptr = { 123, 125 };
}
おおよそ
次のように展開される
struct St {
int64_t a;
int64_t b;
};
int set(St *ptr) {
ptr->a = 123;
ptr->b = 125;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Data race : CPU キャッシュ (1)
41
メインメモリ (DRAM)
L3 キャッシュ
L1 キャッシュ
L2 キャッシュL2 キャッシュ
L1 キャッシュ
Core 1 Core 2
○ メモリアクセスは遅いのでキャッ
シュ階層を設ける
○ 容量と速度の要請から複数段に分か
れている
○ Core に近いキャッシュは Core ごと
に用意されている
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Data race : CPU キャッシュ (2)
42
メインメモリ (DRAM)
L3 キャッシュ
L1 キャッシュ
L2 キャッシュL2 キャッシュ
L1 キャッシュ
Core 1 Core 2
○ ある Core でのメモリ書き込みが他
の Core に見えるまでには時間がか
かる
○ 左図では最速 44 clock 先
○ 異なる Core で同じアドレスを読み
込んでいた場合、自分のキャッシュ
にある値を読み込み書き込みに気が
付かない4 clocks
11 clocks
22 clocks
たくさん
レイテンシ
write
時間をかけて伝わる
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
CPU に必要な機能
○ メモリを直接操作する
○ ある Core での操作が他の Core にすぐ見える
○ キャッシュの整合性を保つ
○他の Core をストールさせる
○キャッシュ間の同期をとる
43Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Intel X86 CPU 命令
○ LOCK XADD [mem] r
○ r = [mem], [mem] = [mem] + r
○ LOCK CMPXCHG [mem] r eax
○ if (eax == [mem]) { [mem] = r } else { eax = [mem] }
○ これらの命令を使うことで core 間の同期をとりつつ処理できる
44Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
C++ atomic 変数
○ Atomic CPU 命令をラップしてくれるクラス
○ 変数への操作を適切な atomic CPU 命令に変換してくれる
○ アーキテクチャによって提供されるビット幅等が違う
45
std::atomic<int> val = 128;
void func() {
int chk = 256;
val.compare_exchange_weak(chk, 1024); // lock cmpxchg
val++; // lock xadd
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
C++ atomic 変数
○ 利点 : data race を除去できる
○ 欠点 : ある程度のオーバーヘッドがある
○ 常にメモリ上に変数が配置されるうえに Cache の整合性を保つ処理が入る
○同じキャッシュラインへの操作はストールする
○ 使い方
○ #include <atomic> する
○ atomic にしたい変数を std::atomic<type> とする
○アーキテクチャによって type に入れれる型は違う
46Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
応用 :lock-free アルゴリズム
○ Compare and Swap (CAS) を利用して読み取り値が変わらない場合のみ操作成功
させる
○ Intel CPU の命令は CMPXCHG だが一般的な用語は Compare and Swap
○ 操作中に割り込みが入った場合 CAS が成功しないので (事前に読み取った値と異な
る値が返される) 成功するまでリトライを繰り返す
47Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Lock – free アルゴリズム例
○ 現在値が引く値よりも大きければ引く操作
○ Compare and exchange に成功すれば ret には tmp が入るはず
○ (ret == tmp) で成功判定できる
48
std::atomic<int> val = 128;
void minus(int sub) {
int tmp = 0; int ret = 0;
do {
ret = tmp = val;
if (sub <= tmp) {
ret = val.compare_exchange_weak(tmp, tmp - sub);
}
} while (ret != tmp);
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
応用 : SpinLock
○ Mutex と機能は同じだがビジーウェイトによってロック解放を待つ
○ ロック対象処理が短い場合に Mutex よりも反応性がよい
○ シングルコア CPU では意味がない
○ 実装は atomic 変数の操作で行う
49Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
SpinLock 実装
50
class spinlock {
private:
std::atomic_flag state_;
public:
spinlock() : state_(ATOMIC_FLAG_INIT) {}
void lock() {
// busy-wait で現在の状態をロック状態にする
while (state_.test_and_set(std::memory_order_acquire));
}
void unlock() {
state_.clear(std::memory_order_release); // 値をアンロック状態にする
}
};
int func() {
static spinlock lock;
std::lock_guard<spinlock> lk(lock);
static int val = 0; return val++;
}
Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
Introduction to conccrent_lock

Introduction to conccrent_lock

  • 1.
    Concurrent 同期入門 株式会社ピクセラ 製品事業本部 ソフトウェア開発部門 先端技術開発部 © 2017PIXELA CORPORATION|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL. 2019年6月21日
  • 2.
    発表の目的 ○ マルチスレッドプログラミングにおける競合について知ってもらう ○ 同期オブジェクトについて知ってもらう ○マルチスレッド設計/実装のノウハウを知ってもらう 2Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 3.
    アジェンダ ○ 1: マルチスレッドクイズ ○2: 競合とその原因と対策 ○ 3: 同期オブジェクト ○ 4: 設計/実装上の注意 ○ 5: Atomic 変数操作 3Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 4.
    1:マルチスレッドクイズ 4Copyright © PIXELACORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 5.
    このコードの x がとりうる値は? 5 intx = 0; void func1() { for (int i = 0; i < 1000000; i++) x++; } int main() { std::thread t1([]() { func1(); }), t2([]() { func1(); }); t1.join(); t2.join(); printf("x:%dn", x); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 6.
    2:競合とその原因と対策 6Copyright © PIXELACORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 7.
    競合 (1) :データ競合 (data race) ○ 複数スレッドで同じ変数に対して同時に Read/Write が発生する ○ 厳密には少なくとも一つが Write となる ○ 発生現象 ○ 何が発生するか予想できない ○変更途中の値が Read (変数がレジスタ幅を超える場合) ○Write 順が他のスレッドの Read に反映されない ○ 原因 ○コードと CPU 命令の乖離 ○CPU の内部動作 (キャッシュ) 7Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 8.
    競合 (1) :データ競合 (data race) 8 int my_account = 0; // 私の預金口座残高 int your_account = 100; // あなたの預金口座残高 // 送金処理 bool racy_transfer(int m) { if (m <= my_account) { // 未定義動作の可能性あり! my_account -= m; // 未定義動作の可能性あり! your_account += m; // 未定義動作の可能性あり! return true; } return false; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 9.
    競合 (2) :競合状態 (race condition) ○ 実行タイミングに依存してシステムの出力結果が変化する ○ 発生現象 ○ 変数が不変条件に違反した値になる ○ タイミングによって不具合発生、sleep を入れるとクラッシュ ○ 原因 ○ 設計/実装ミス ○複数のリソースに対する操作が排他されていない ○スレッド間の調停を行っていない 9Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 10.
    競合 (2) :競合状態 (race condition) 10 std::atomic<int> my_account = 0; // 私の預金口座残高 std::atomic<int> your_account = 100; // あなたの預金口座残高 // 送金処理 bool unsafe_transfer(int m) { if (m <= my_account) { // ★この時点でも(m <= my_account)は真? my_account -= m; your_account += m; return true; } return false; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 11.
    競合への対策 (基本) ○ 操作を不可分(atomic) とする ○ atomic とは一連の操作が他スレッドから一体と見えること ○全操作が完了するまで他スレッドから変更が見えない ○操作失敗時には操作前の状態に戻る ○複数スレッドで同時に操作できない ○ 例: 複数スレッドからアクセスされるリソースを排他する ○ 前提条件を満たしてから実行する ○ 他のスレッドの入力を待つような場合 11Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 12.
    正しいコード 12 int my_account =0; // 私の預金口座残高 int your_account = 100; // あなたの預金口座残高 std::mutex txn_guard; // 送金処理: 安全なトランザクション処理 bool safe_transfer(int m) { std::lock_guard<std::mutex> lk(txn_guard); if (m <= my_account) { my_account -= m; your_account += m; return true; } return false; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 13.
    3:同期オブジェクト 13Copyright © PIXELACORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 14.
    代表的な同期オブジェクト ○ Mutex ○ Read-WriteLock ○ Condition Variable ○ Event ○ Semaphore ○ Volatile (正確には同期オブジェクトではない) 14Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 15.
    Mutex ○ ロックすることでそれ以降のコードへのアクセス権を取得する ○ 同時にロックできるのは一つのスレッドのみ ○同じ Mutex をロックしているコード間は排他されている ○ Mutex の作り方によって同じスレッドが複数回ロックをとれる (Recursive) なもの とできないものがある ○ パフォーマンスが要求されない限り Recursive にするのが安全 ○後の修正で Recursive が必要になるかもという理由 15Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 16.
    Mutex コード例 (C++) 16 intfunc() { static std::recursive_mutex mutex; std::lock_guard<std::recursive_mutex> lock(mutex); static int count = 0; count++; return count; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 17.
    Read-Write Lock ○ Readは複数あっても race condition は起きないことを利用してパフォーマンス向 上を行う ○ Read のロック API と Write API のロックが分かれている ○ Read だけなら複数取得可 ○ Write と Read, 複数の Write は同時に行えない ○ Write に比べて Read が多い場合にロック待ち時間を削減できる 17Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 18.
    Read-Write Lock コード例(Win32 API) 18 void init(SRWLOCK* lock) { InitializeSRWLock(lock); } static int count = 0; int read(SRWLOCK* lock) { // AcquireSRWLockExclusive がない限り通る AcquireSRWLockShared(lock); int ret = count; ReleaseSRWLockShared(lock); return ret; } int write(int val, SRWLOCK* lock) { // 他の AcquireSRWLockExclusive, AcquireSRWLockShared がない限り通る AcquireSRWLockExclusive(lock); count = val; ReleaseSRWLockExclusive(lock); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 19.
    Condition Variable ○ 待機側が待機し、通知側が待機側を起動させる ○待機側が待機前にロックオブジェクト (Mutex, Read-Write Lock) を握っているの が前提となる ○ 待機と不可分に行える ○ “条件変数” とよばれているのは以下のような使われ方のため ○ 待機側は処理条件を満たしていない場合に待機する ○ 通知側は待機側の処理条件を満たした状態にして通知する 19Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 20.
    Condition Variable コード例(C++) 20 static std::mutex mutex; static std::condition_variable cond; bool flag = false; void wait() { // flag が true になるまで待機。 std::unique_lock<std::mutex> lock(mutex); cond.wait(lock, []() { return flag; }); flag = false; } void wake() { // 待機スレッドを起こす { std::unique_lock<std::mutex> lock(mutex); flag = true; } cond.notify_one(); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 21.
    Event ○ 通知側が通知を行うと待機しているスレッドが起動する ○ オブジェクトのSignal (通ってよし) と Non-Signal (通行止め) を管理する ○ Signal 状態でロックとると non-Signal になる/ならないとか、初期状態セットでき るとかバリエーションがいっぱいある ○ ので使う際には OS のリファレンスをちゃんと読む必要がある ○ 典型的な例は I/O 待ち 21Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 22.
    Event コード例 (Win32API) 22 int init(HANDLE *handle) { // 自動で non-signal になる初期値 non-signal のイベント作成 *handle = CreateEvent(/*atrtributes*/NULL,/*manual_reset*/FALSE, /*initial_state*/FALSE, /*name*/NULL ); } void wait(HANDLE handle) { // signal 状態まで待つ // 自動で non-signal になるので一度に起動するスレッドはひとつ WaitForSingleObject(handle, INFINITE); } void wake(HANDLE handle) { SetEvent(handle); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 23.
    Semaphore ○ 指定回数だけ取得できる ○ 解放は取得者でなくてもよい ○リソースの共有数の管理に使えるはず ○ Mutex と条件変数でも実装できるが… 23Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 24.
    Semaphore コード例 (Win32API) 24 int func() { static HANDLE sem = CreateSemaphore( /*attributes*/NULL, /*initial count*/5, /*max count*/ 12, /*name*/ NULL ); WaitForSingleObject(sem, INFINITE); // count--, count > 0 でロックされない // 処理 // count 増加 -> 待機スレッド起動 ReleaseSemaphore(sem, /*count*/1, /*[out] prev_count*/NULL); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 25.
    Volatile ○ コンパイラによる最適化を防ぐ ○ 変数アクセス時に逐次メモリを読みに行く ○ループ中のスレッド外部での変数の変更の検出に使える ○関数内での命令リオーダーは防げない ○コードの記述順に Read/Write されると期待すべきではな い ○ Volatile ではなく適切な atomic 機構に頼るべき ○ Volatile を使うケースは以下 ○ 外部ハードウェアデバイスを参照している場合 25Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 26.
    Volatile コード例 (C++) 26 //デバイス側で変更されるフラグ extern volatile int flag; int wait() { // busy-loop でチェック // 外部で flag が true になれば抜ける while (!flag) {}; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 27.
    注意点 ○ 各同期オブジェクトが使用できる/できないは OSや言語によって違う ○ 各同期オブジェクトがプロセス間で共有できる/できないは OS 依存 ○ C++ の各オブジェクト操作が OS のどの API に相当するかは stdlib の実装依存となっ ている ○ パフォーマンスが必要なら軽い API に切り替える必要がある ○ スペースの都合上ヘッダの include は省略した ○ 共有オブジェクトが static 変数になっているのはスペースの都合上なので実装ではやら ないこと 27Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 28.
    4:設計/実装上の注意 28Copyright © PIXELACORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 29.
    生のロックオブジェクトは扱わない ○ ロックオブジェクトを操作するコードを直接書かない ○ アンロックするコードをロック区間出口の数だけ書く必要性 ○C++ なら std::lock_guard 等を使う ○ RAII (Resource Acquisition Is Initialization) ○ スタック上にのったオブジェクトの破棄時にリソース破棄が発生 ○ C の場合 RAII は使えないので goto パターンでアンロックを一か所に集めること 29Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 30.
    条件変数の spurious wakeup ○条件変数での待機処理にて通知なしでブロック解除される現象 ○ 原因はライブラリ内部処理やハードウェア/OS の都合 ○ 発生頻度はごく低い…はず ○ 対応 : 条件変数を使う場合は必ず待機終了後に待機条件をチェック ○ 満たしていない場合は再度待機 30Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 31.
    デッドロックを防ぐために – 原因 ○デッドロックは 2 個以上のロックの取得順番が違うときに発生する ○ 特に危ないケース ○ ロックを取得したまま他モジュールを呼びに行き、そのモジュールから自モ ジュールを呼び出す経路がある場合 ○同じスレッドでの呼び出しは問題ないが、異なるスレッドで の呼び出し経路があると最悪 ○ モジュール内に複数ロックオブジェクトがある場合 ○常にロック順序を一定にする必要がある 31Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 32.
    デッドロックを防ぐために – 状況 32Copyright© PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 33.
    デッドロックを防ぐために – 対策 ○排他区間を短くする ○ モジュール内のロックオブジェクトを一つにする ○ モジュール相互の呼び出しが発生するパスを作らない ○ ロックを取得したまま他モジュールを呼ばない ○ 上位モジュールの処理は上位モジュールのスレッドで処理する ○ いったん上位モジュールのタスクキューで受ける等 ○タスクキューが詰まってデッドロックになる場合も (実例) ○ 下位モジュールのロックを処理から切り離す 33Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 34.
    Sleep 時間はベストエフォート ○ 各種sleep/待機関数に渡す時間はその時間での起動を保証しない ○ スレッドスケジューリング (スレッド優先度等) の都合上起動が待たされる可能性が ある ○ また、起動の精度はタイマー割り込みの頻度に依存する ○ Windows: 16 millisecond (デフォルト状態) ○ PC Linux: 4 millisecond (2010 の情報) 34Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 35.
    精密な Sleep 時間が必要な場合 ○精密に起動できるようにする ○ タイマー割り込み頻度の変更 (1 msec 精度まで) ○ スレッド優先度の引き上げ ○ Busy loop の採用 35Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 36.
    5:Atomic 変数操作 36Copyright ©PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 37.
    ロックによるオーバヘッド ○ 頻繁にアクセスする変数にロックをかけるとパフォーマンス低下 ○ 頻繁な状態チェック ○API 呼び出し -> カーネル空間への移動 ○ メモリを Atomic に操作する変数 (正確にはメモリを Atomic に操作する CPU 命 令) を使えばパフォーマンスが向上する 37Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 38.
    Atomic 変数の前に –data race の原因 ○ コードと CPU 命令の乖離 ○ CPU 内部の都合 ○ キャッシュの同期が基本行われない 38Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 39.
    Data race :コードと CPU 命令の乖離 (1) ○ 下の処理中でコンテキストスイッチとなった場合、 再開時は昔のメモリを読み込んだ状態で処理再開 される 39 int func(int *ptr) { *ptr++; } int func(int *ptr) { int val = *ptr; val++; *ptr = val; } おおよそ 次のように展開される Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 40.
    Data race :コードと CPU 命令の乖離 (2) ○ 変数がレジスタ幅に収まらない場合、メモリ書き込 み途中で他スレッドから読み取られる可能性がある ○ 矛盾した状態を読み込んでしまう 40 struct St { int64_t a; int64_t b; }; int set(St *ptr) { *ptr = { 123, 125 }; } おおよそ 次のように展開される struct St { int64_t a; int64_t b; }; int set(St *ptr) { ptr->a = 123; ptr->b = 125; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 41.
    Data race :CPU キャッシュ (1) 41 メインメモリ (DRAM) L3 キャッシュ L1 キャッシュ L2 キャッシュL2 キャッシュ L1 キャッシュ Core 1 Core 2 ○ メモリアクセスは遅いのでキャッ シュ階層を設ける ○ 容量と速度の要請から複数段に分か れている ○ Core に近いキャッシュは Core ごと に用意されている Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 42.
    Data race :CPU キャッシュ (2) 42 メインメモリ (DRAM) L3 キャッシュ L1 キャッシュ L2 キャッシュL2 キャッシュ L1 キャッシュ Core 1 Core 2 ○ ある Core でのメモリ書き込みが他 の Core に見えるまでには時間がか かる ○ 左図では最速 44 clock 先 ○ 異なる Core で同じアドレスを読み 込んでいた場合、自分のキャッシュ にある値を読み込み書き込みに気が 付かない4 clocks 11 clocks 22 clocks たくさん レイテンシ write 時間をかけて伝わる Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 43.
    CPU に必要な機能 ○ メモリを直接操作する ○ある Core での操作が他の Core にすぐ見える ○ キャッシュの整合性を保つ ○他の Core をストールさせる ○キャッシュ間の同期をとる 43Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 44.
    Intel X86 CPU命令 ○ LOCK XADD [mem] r ○ r = [mem], [mem] = [mem] + r ○ LOCK CMPXCHG [mem] r eax ○ if (eax == [mem]) { [mem] = r } else { eax = [mem] } ○ これらの命令を使うことで core 間の同期をとりつつ処理できる 44Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 45.
    C++ atomic 変数 ○Atomic CPU 命令をラップしてくれるクラス ○ 変数への操作を適切な atomic CPU 命令に変換してくれる ○ アーキテクチャによって提供されるビット幅等が違う 45 std::atomic<int> val = 128; void func() { int chk = 256; val.compare_exchange_weak(chk, 1024); // lock cmpxchg val++; // lock xadd } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 46.
    C++ atomic 変数 ○利点 : data race を除去できる ○ 欠点 : ある程度のオーバーヘッドがある ○ 常にメモリ上に変数が配置されるうえに Cache の整合性を保つ処理が入る ○同じキャッシュラインへの操作はストールする ○ 使い方 ○ #include <atomic> する ○ atomic にしたい変数を std::atomic<type> とする ○アーキテクチャによって type に入れれる型は違う 46Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 47.
    応用 :lock-free アルゴリズム ○Compare and Swap (CAS) を利用して読み取り値が変わらない場合のみ操作成功 させる ○ Intel CPU の命令は CMPXCHG だが一般的な用語は Compare and Swap ○ 操作中に割り込みが入った場合 CAS が成功しないので (事前に読み取った値と異な る値が返される) 成功するまでリトライを繰り返す 47Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 48.
    Lock – freeアルゴリズム例 ○ 現在値が引く値よりも大きければ引く操作 ○ Compare and exchange に成功すれば ret には tmp が入るはず ○ (ret == tmp) で成功判定できる 48 std::atomic<int> val = 128; void minus(int sub) { int tmp = 0; int ret = 0; do { ret = tmp = val; if (sub <= tmp) { ret = val.compare_exchange_weak(tmp, tmp - sub); } } while (ret != tmp); } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 49.
    応用 : SpinLock ○Mutex と機能は同じだがビジーウェイトによってロック解放を待つ ○ ロック対象処理が短い場合に Mutex よりも反応性がよい ○ シングルコア CPU では意味がない ○ 実装は atomic 変数の操作で行う 49Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.
  • 50.
    SpinLock 実装 50 class spinlock{ private: std::atomic_flag state_; public: spinlock() : state_(ATOMIC_FLAG_INIT) {} void lock() { // busy-wait で現在の状態をロック状態にする while (state_.test_and_set(std::memory_order_acquire)); } void unlock() { state_.clear(std::memory_order_release); // 値をアンロック状態にする } }; int func() { static spinlock lock; std::lock_guard<spinlock> lk(lock); static int val = 0; return val++; } Copyright © PIXELA CORPORATION. All Rights Reserved.|PIXELA CORPORATION PROPRIETARY AND CONFIDENTIAL.