[超初心者向け!]Lock 実装を理論からわかりやすく解説 [Two Phase Locking / Compare and Swap]
はじめに
Lock 実装をコーディング初心者でも分かるように解説してみました
この記事の対象者
・トランザクションの記事を読んでくれた人
・B+treeが実装できた人
一緒に実装しましょう!
トランザクションの記事はこちらから
目次
- processとthreadの違い
- なぜ Lock がいるのか
- Lock の種類
- コード解説
コードの書き方はあくまで一例です
絶対的な解ではありませんので、参考にしていただければ幸いです
1. processとthreadの違い
複数の CPU に分けて処理するため、process から thread を作成します
複数あるスレッド (マルチスレッド) を複数の CPU (マルチプロセッサ) が操作することで より高速に処理を終えることができます
2. なぜ Lock がいるのか
同時にメモリから読み込んで、別々の CPU (マルチプロセッサ)で処理をしてそれぞれメモリに書き込む場合、本来なって欲しい値と違う値がメモリに記録されてしまうことがあります
これを Lost Update といいます
このような不整合 (アノマリー) をなくすためには Lock が必要です
3. Lock の種類
2PLでは (Two phase Locking) 2種類のロックが使用されます
Shared lock
トランザクションが読み込むデータアイテムに使用されます
同じデータアイテムに対して、複数の Shared lockを取ることができます
読むだけだったら、値が変わることはないためいくつロックを取ったとしてもアノマリーが発生することはないからです
しかし Shared lockを取った後に Exclusive lockを取ることはできません
Exclusive lock
トランザクションが書き込むデータアイテムに使用されます
同じデータアイテムに対して、Exclusive lockを取った後にExclusive lockを取ることはできません
また、Exclusive lockを取った後にShared lockを取ることはできません
4. コード解説
1. Exclusive lock
まずはとりあえず実装を見てみましょう
void lock()
{
int64_t expected;
while (true)
{
expected = __atomic_load_n(&cnt, __ATOMIC_SEQ_CST); // 値はatomicにloadする必要がある
if (expected == 0 && __atomic_compare_exchange_n(&cnt, &expected, -1, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
{
return;
}
}
}
lockする時の手順はこちらです
①lockの値をアドミックに読む
②Compare and Swap して、もし書くまえにもう一回読んでloadした値と一緒だったら書く
③もしlockがとれなかった場合ループする
②アトミックに読むとは?
メモリ階層は優先度が高い順にレジスタ、キャッシュ、メモリです
CPU にはレジスタとキャッシュが入っています
複数のCPU(マルチプロセッサ)の場合、データがどこに配置されるかはOSが自動で設定します(paging)
そのため、もしcacheに入ってしまうと、他のCPUが読めなくなってしまいます
これにより変更内容は必ずメモリに配置されるようにするためアトミックにロックを外す必要があります
③Compare and Swap とは?
データを変更するまえにもう一回読んで、もしloadした値と一緒だったら書くことです
このように、ロックカウンターを同時にロードしてから書き換えた場合、本来は一個しか取れないロックが二つ取れてしまうという状況になってしまいます
そこで Compare and swap を導入することで、ロックが必ず1つしか取れないようにします
Compare and swap の手順は以下の通りです
①loadする
②書くまえにもう一回読んでloadした値と一緒だったら書く
こうすることによって、読んでから書く間に書き換えられている心配がありません
2. unlock
void unlock()
{
//__atomic_fetch_add(&cnt, 1, __ATOMIC_SEQ_CST); -1 => 0
__atomic_store_n(&cnt, 0, __ATOMIC_SEQ_CST);
// この実装だと壊れる
// int cur_cnt = cnt; // load
// cnt = cur_cnt + 1; // store
//
} // unlock: 1
手順はこちらです
①アトミックにロックを外す
こちらも上記に記した通り、メモリ階層の関係でアトミックに外す必要があります。(変更内容が必ずメモリに入るようにしないと他の CPU が読むことができなくなってしまう)
3. Shared lock
void lock_shared()
{
int64_t expected;
while (true)
{
expected = __atomic_load_n(&cnt, __ATOMIC_SEQ_CST);
if (expected >= 0 && __atomic_compare_exchange_n(&cnt, &expected, expected + 1, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
{
return;
}
Shared lock はとられるたびに、Lock counter が1ずつ増えます
Exclusive lockはとられるとLock counterが-1になる
つまり、Lock counter の状態をまとめると以下のようになります
Shared lockが取られている状態: Shared lock毎に+1
なにもLockが取られていない状態: 0
Exclusive lock: -1
最後に Exclusive lockだけではありますが、コードを載せておきます。ぜひ実行してみてください
#include <cassert>
#include <unordered_map>
#include <thread>
#include <vector>
#include <iostream>
#include <stdio.h>
class Lock
{
public:
Lock()
: cnt(0) {}
void initialize()
{
cnt = 0; // if locked: -1, if unlocked: 0
}
void lock()
{
int64_t expected;
while (true)
{
expected = __atomic_load_n(&cnt, __ATOMIC_SEQ_CST); // 値はatomicにloadする必要がある
if (expected == 0 && __atomic_compare_exchange_n(&cnt, &expected, -1, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
{
return;
}
}
}
void unlock()
{
//__atomic_fetch_add(&cnt, 1, __ATOMIC_SEQ_CST); -1 => 0
__atomic_store_n(&cnt, 0, __ATOMIC_SEQ_CST);
// この実装だと壊れる
// int cur_cnt = cnt; // load
// cnt = cur_cnt + 1; // store
//
} // unlock: 1
private:
int64_t cnt = 0;
// cnt==-1 誰かがlockを取っている
// cnt==0 誰もlockをとっていない
};
int river_tshirt = 100;
Lock lock;
void function()
{
// river_tshirt--;
lock.lock(); // acquire lock
int current_stock = river_tshirt;
river_tshirt = current_stock - 1;
lock.unlock(); // release lock
}
int main()
{
int thread_num = 100;
std::vector<std::thread> threads;
for (int i = 0; i < thread_num; i++)
{
threads.emplace_back(function); // start thread
}
for (int i = 0; i < thread_num; i++)
{
threads[i].join(); // end thread
}
std::cout << river_tshirt << std::endl;
}
// tony: function() load:3 store:2
// risa: function() load:3 store:2
// rina: function() load:2 store:1
// コードの説明
// ①100個tshirtがある
// ②100個のthreadが一つずつtshirtを購入
// ③最終的に0個になり0が出力される
最後まで読んでくださりありがとうございました!
Discussion