並行プログラミングおよびその応用
pthread (POSIXスレッド)
互斥ロックとスピンロック
Mutex がロックを取得する際に、そのロックが他のスレッドによって保持されている場合、ロックを取得しようとするスレッドはブロックされ、コンテキストスイッチを行い、待機キューに配置されます。この際、CPU は他の処理を実行することができ、ロックの所有権が空いた後に再びそのスレッドが実行されます。
Spin はロックを取得できない場合、常にバスイ待ちを行い、ロックが取得できるまで待機します。
具体的には、ロックの取得に時間のかかる場合は Mutex を、ロックの取得に時間が短い場合は Spin を使用します。
条件変数
std::condition_variable
使用する関数:
-
pthread_cond_init/destroyは互斥ロックやスピンロックと同様 -
pthread_cond_wait
関数定義:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)cond: 条件変数へのポインタmutex: 互斥ロックへのポインタ
この関数を呼び出す前に、まず互斥ロック
mutexを取得する必要があります。待機中は自動的にロックが解放され、現在のスレッドはブロック状態となり、条件変数が通知され、再びロックが取得されるまで待機します。 -
pthread_cond_signal/pthread_cond_broadcast
条件変数を待機しているスレッドを起こすために使用されます。signal: 条件変数を待機している複数のスレッドのうち、1つのスレッドを起こしますbroadcast: 条件変数を待機している全てのスレッドを起こします
もし条件変数を待機しているスレッドが存在しない場合、この関数は何の効果も生じません。以下は、C++のスマートポインタ(
shared_ptr)と並行性(スレッドセーフネス)についての説明を日本語に翻訳したものです。
スレッドセーフなスマートポインタについて
1. shared_ptr のスレッドセーフ性について
shared_ptr自体は、単純な読み取り操作(参照カウントの読み取りやコピー)はスレッドセーフではありません。これは、参照カウントの増減やメモリの解放など、複数の操作が原子的に行われないためです。shared_ptrの拷べ操作(コピー構築や代入)は、参照カウントの増減が原子的に行われるため、スレッドセーフです。ただし、その操作自体はスレッドセーフではありませんが、参照カウントの増減部分はスレッドセーフです。
2. shared_ptr のスレッドセーフな利用方法
shared_ptr をスレッドセーフに使用するには、以下の点に注意する必要があります。
- 参照カウントの読み取りのみ:参照カウントの読み取りはスレッドセーフです(
use_count()メソッド)。 - スレッドセーフな操作の保証:
shared_ptrの拷べ操作(コピーや代入)は、参照カウントの増減が原子的に行われるため、スレッドセーフです。 - スレッドセーフなメモリ管理:
shared_ptrの解放(メモリの解放)は、参照カウントがゼロになるまで行われるため、スレッドセーフです。
3. 並行性を考慮した shared_ptr の実装例
以下は、スレッドセーフな shared_ptr の実装例です。この実装では、std::atomic を用いて参照カウントを管理しています。
#include <iostream>
#include <atomic>
class Counter {
public:
Counter() : count(1) {}
void add() { count++; }
void sub() { count--; }
int get() const { return count; }
private:
std::atomic<int> count;
};
template <typename T>
class Sp {
public:
Sp() : my_ptr(nullptr), counter(new Counter()) {}
Sp(T* ptr) : my_ptr(ptr), counter(new Counter()) {}
Sp(const Sp& obj) : my_ptr(obj.my_ptr), counter(obj.counter) {
counter->add();
}
~Sp() { clear(); }
Sp& operator=(const Sp& obj) {
clear();
my_ptr = obj.my_ptr;
counter = obj.counter;
counter->add();
return *this;
}
T* get() const { return my_ptr; }
int get_count() const { return counter->get(); }
private:
T* my_ptr;
Counter* counter;
void clear() {
counter->sub();
if (counter->get() == 0) {
if (my_ptr) {
delete my_ptr;
}
delete counter;
}
}
};
4. 並行性を考慮した shared_ptr の使用例
スレッドセーフな shared_ptr を使って、スレッド間で共有するオブジェクトを管理する例を示します。
#include <iostream>
#include <thread>
int main() {
Sp<int> shared_ptr(new int(42));
std::thread t1([&shared_ptr]() {
std::cout << "Thread 1: Value = " << *shared_ptr.get() << std::endl;
std::cout << "Thread 1: Reference count = " << shared_ptr.get_count() << std::endl;
});
std::thread t2([&shared_ptr]() {
Sp<int> shared_ptr_copy = shared_ptr;
*shared_ptr_copy.get() = 100;
std::cout << "Thread 2: Value = " << *shared_ptr.get() << std::endl;
std::cout << "Thread 2: Reference count = " << shared_ptr.get_count() << std::endl;
});
t1.join();
t2.join();
return 0;
}
5. 重要な注意事項
shared_ptrのスレッドセーフ性は、参照カウントの操作が原子的に行われることに依存します。- 参照カウントの読み取りはスレッドセーフですが、参照カウントを変更する操作はスレッドセーフではありません。
shared_ptrを使用する際は、スレッド間で共有するオブジェクトの変更を適切に同期させる必要があります。
6. 並行性を考慮した shared_ptr の使用上のベストプラクティス
- スレッドセーフな共有オブジェクトの管理:スレッド間で共有するオブジェクトの変更は、ミューテックスや原子操作を用いて同期させる必要があります。
- スレッドセーフな参照カウントの使用:
shared_ptrの参照カウントは、スレッドセーフに操作されるように設計されていますが、オブジェクト自体の変更はスレッドセーフではありません。
7. 並行性を考慮した shared_ptr の実装の詳細
std::atomicを使用することで、参照カウントの増減をスレッドセーフに行うことができます。- 参照カウントの読み取りは、スレッドセーフです。
- 参照カウントの増減は、原子的に行われるため、スレッドセーフです。
まとめ
shared_ptrは、参照カウントの操作がスレッドセーフです。- 参照カウントを変更する操作は、スレッドセーフです。
- スレッド間で共有するオブジェクトの変更は、ミューテックスや原子操作を用いて同期させる必要があります。
これらの点を理解し、適切に使用することで、スレッドセーフな shared_ptr を実現することができます。```cpp
template
T * sp::get()
{
return my_ptr;
}