マルチスレッドプログラミングにおいて、スレッド間のデータ競合を防ぐためには同期が欠かせません。C++では、この同期を行うためのツールとしてstd::mutexとstd::lock_guardが用意されています。この記事では、これらのツールを使ったスレッド同期の方法を、具体的なコード例とともに分かりやすく解説します。初心者から上級者まで、スレッド同期の基本から応用までを学ぶことができる内容となっています。
std::mutexの基本的な使い方
C++のstd::mutexは、スレッド間で共有されるデータへのアクセスを制御するための基本的なロック機構です。以下に、std::mutexの基本的な使い方を説明します。
std::mutexの宣言と初期化
std::mutexを使うには、まずそれを宣言します。これは通常、スレッド間で共有されるリソースと一緒に宣言されます。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // mutexの宣言
void print_thread_id(int id) {
mtx.lock(); // ロックの取得
std::cout << "Thread ID: " << id << std::endl;
mtx.unlock(); // ロックの解放
}
int main() {
std::thread t1(print_thread_id, 1);
std::thread t2(print_thread_id, 2);
t1.join();
t2.join();
return 0;
}
ロックの取得と解放
上記の例では、mtx.lock()でロックを取得し、共有リソースへのアクセスが完了したらmtx.unlock()でロックを解放しています。これにより、print_thread_id関数が同時に複数のスレッドから実行される場合でも、共有リソース(ここではstd::cout)へのアクセスが直列化されます。
例外処理とstd::mutex
手動でロックを取得し解放する場合、例外が発生したときにunlockが呼ばれない可能性があるため注意が必要です。この問題を解決するために、次に紹介するstd::lock_guardを使用することが推奨されます。
std::lock_guardの基本的な使い方
std::lock_guardは、C++の標準ライブラリが提供するRAII (Resource Acquisition Is Initialization) メカニズムを使ったロック管理のためのユーティリティです。これにより、コードの安全性が向上し、ロックの取得と解放を自動的に行います。
std::lock_guardの宣言と使用
std::lock_guardを使用するには、std::mutexオブジェクトを引数にしてstd::lock_guardを宣言します。std::lock_guardがスコープに入るときにロックが取得され、スコープから出るときに自動的にロックが解放されます。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // mutexの宣言
void print_thread_id(int id) {
std::lock_guard<std::mutex> lock(mtx); // ロックの取得と管理
std::cout << "Thread ID: " << id << std::endl;
}
int main() {
std::thread t1(print_thread_id, 1);
std::thread t2(print_thread_id, 2);
t1.join();
t2.join();
return 0;
}
自動的なロック解放
std::lock_guardを使用することで、ロックの取得と解放が自動的に管理されます。上記の例では、print_thread_id関数内でstd::lock_guardが宣言されると同時にロックが取得され、関数が終了するときに自動的にロックが解放されます。これにより、例外が発生した場合でも確実にロックが解放されるため、安全性が向上します。
std::lock_guardの利点
- 簡潔なコード: 明示的にロックとアンロックを記述する必要がなくなります。
- 例外安全: 例外が発生しても自動的にロックが解放されるため、デッドロックを防ぐことができます。
- 読みやすさ: コードの意図が明確になり、読みやすくなります。
std::lock_guardを使用することで、C++におけるスレッド同期がより簡単かつ安全に行えるようになります。
mutexとlock_guardの違い
std::mutexとstd::lock_guardはどちらもC++におけるスレッド同期のためのツールですが、それぞれ異なる特性と使用方法があります。ここでは、それぞれの違いと適用シーンについて詳しく解説します。
基本的な違い
- std::mutex: 手動でロックを取得し、手動でロックを解放する必要があります。より細かい制御が可能ですが、ロックとアンロックの管理をミスするとデッドロックやリソースリークの原因となります。
- std::lock_guard: RAIIパターンを使用しており、スコープに入ると同時にロックを取得し、スコープから出ると自動的にロックを解放します。コードが簡潔になり、例外が発生しても確実にロックが解放されるため、安全性が高まります。
使用例
以下に、std::mutexとstd::lock_guardの使用例を示します。
std::mutexの使用例
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // mutexの宣言
void print_thread_id(int id) {
mtx.lock(); // ロックの取得
std::cout << "Thread ID: " << id << std::endl;
mtx.unlock(); // ロックの解放
}
int main() {
std::thread t1(print_thread_id, 1);
std::thread t2(print_thread_id, 2);
t1.join();
t2.join();
return 0;
}
std::lock_guardの使用例
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // mutexの宣言
void print_thread_id(int id) {
std::lock_guard<std::mutex> lock(mtx); // ロックの取得と管理
std::cout << "Thread ID: " << id << std::endl;
}
int main() {
std::thread t1(print_thread_id, 1);
std::thread t2(print_thread_id, 2);
t1.join();
t2.join();
return 0;
}
適用シーン
- std::mutex: ロックの取得と解放を細かく制御する必要がある場合や、ロックの状態を確認しながら操作を行いたい場合に適しています。
- std::lock_guard: ロックの取得と解放を自動化し、安全かつ簡潔なコードを記述したい場合に適しています。
まとめ
std::mutexは手動による詳細なロック管理が可能であり、std::lock_guardは自動的なロック管理を提供します。これらの特性を理解し、適切な場面で使い分けることで、スレッド同期の問題を効果的に解決することができます。
スレッド同期の実例
ここでは、std::mutexとstd::lock_guardを使用したスレッド同期の具体的なコード例を紹介します。この例では、複数のスレッドが同時に共有データにアクセスするシナリオを示し、スレッド同期の効果を確認します。
例: カウンターの同期
以下のコードでは、複数のスレッドが同じカウンター変数をインクリメントします。スレッドが競合しないようにstd::mutexを使用して同期を取ります。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0; // 共有カウンター変数
std::mutex mtx; // mutexの宣言
void increment_counter(int id) {
for (int i = 0; i < 100; ++i) {
std::lock_guard<std::mutex> lock(mtx); // ロックの取得と管理
++counter; // カウンターのインクリメント
std::cout << "Thread " << id << " incremented counter to " << counter << std::endl;
}
}
int main() {
std::vector<std::thread> threads;
// 10個のスレッドを生成
for (int i = 1; i <= 10; ++i) {
threads.emplace_back(increment_counter, i);
}
// 全てのスレッドの終了を待機
for (auto& th : threads) {
th.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
コードの説明
- 共有カウンター変数:
int counter = 0;
は、すべてのスレッドがアクセスする共有変数です。 - mutexの宣言:
std::mutex mtx;
は、カウンター変数へのアクセスを制御するためのmutexです。 - スレッド関数:
increment_counter(int id)
は、各スレッドがカウンターを100回インクリメントする関数です。std::lock_guardを使用して、スレッドがカウンター変数をインクリメントする前にロックを取得し、インクリメントが完了したら自動的にロックを解放します。 - スレッドの生成:
std::vector<std::thread> threads;
でスレッドのベクターを作成し、10個のスレッドを生成します。 - スレッドの終了待機:
th.join();
で、すべてのスレッドが終了するまでメインスレッドが待機します。
実行結果
このプログラムを実行すると、各スレッドがカウンターをインクリメントしている様子が表示され、最終的にカウンターの値が1000になります。これにより、std::mutexとstd::lock_guardを使ったスレッド同期の効果を確認できます。
Thread 1 incremented counter to 1
Thread 2 incremented counter to 2
...
Thread 10 incremented counter to 1000
Final counter value: 1000
この例から、std::mutexとstd::lock_guardを使用することで、複数のスレッドが競合せずに共有データに安全にアクセスできることが分かります。
デッドロックの回避策
デッドロックは、複数のスレッドが互いにロックを待ち続ける状態で、プログラムの実行が停止してしまう問題です。ここでは、デッドロックを防ぐためのベストプラクティスとその実践方法を解説します。
デッドロックの原因
デッドロックは、以下の4つの条件が同時に満たされると発生します。
- 相互排他: リソースは複数のスレッドに同時に使用されない。
- 占有と待機: リソースを保持したまま他のリソースを待機する。
- 不可奪性: 保持しているリソースを他のスレッドが強制的に奪えない。
- 循環待機: スレッド間で循環的にリソースを待機する。
回避策1: ロックの順序を統一する
複数のリソースをロックする場合、すべてのスレッドが同じ順序でロックを取得するようにします。これにより、循環待機が防止されます。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread_function(int id) {
if (id % 2 == 0) {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock2(mtx2);
} else {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock1(mtx1);
}
std::cout << "Thread " << id << " completed" << std::endl;
}
int main() {
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
t1.join();
t2.join();
return 0;
}
上記のコードでは、偶数スレッドと奇数スレッドが異なる順序でロックを取得し、デッドロックの原因となります。すべてのスレッドが同じ順序でロックを取得するように修正します。
回避策2: std::lockを使用する
C++11では、複数のmutexを一度にロックするためにstd::lockを使用できます。これにより、デッドロックを回避できます。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread_function(int id) {
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
std::cout << "Thread " << id << " completed" << std::endl;
}
int main() {
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
t1.join();
t2.join();
return 0;
}
回避策3: タイムアウトを設定する
std::unique_lockとstd::mutexのtry_lock関数を使用して、一定時間後にロックを取得できなければ他の処理に移るようにします。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx1, mtx2;
void thread_function(int id) {
while (true) {
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
if (std::try_lock(lock1, lock2) == -1) {
std::cout << "Thread " << id << " acquired locks" << std::endl;
break;
} else {
std::cout << "Thread " << id << " failed to acquire locks, retrying" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
int main() {
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
t1.join();
t2.join();
return 0;
}
まとめ
デッドロックは、マルチスレッドプログラミングにおける深刻な問題ですが、適切な回避策を講じることで防ぐことができます。ロックの順序を統一する、std::lockを使用する、タイムアウトを設定するなどの方法を用いることで、安全なスレッド同期を実現しましょう。
パフォーマンスへの影響
スレッド同期は、マルチスレッドプログラミングにおいて必須の技術ですが、その使用方法によってはプログラムのパフォーマンスに大きな影響を与えることがあります。ここでは、スレッド同期がプログラムのパフォーマンスに与える影響とその対策について説明します。
ロックの競合による待機時間
複数のスレッドが同じリソースにアクセスしようとすると、ロックの競合が発生します。この競合が頻繁に起こると、スレッドがロックを取得するために待機する時間が増え、プログラム全体のパフォーマンスが低下します。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
std::mutex mtx; // mutexの宣言
int shared_resource = 0;
void increment_resource(int iterations) {
for (int i = 0; i < iterations; ++i) {
std::lock_guard<std::mutex> lock(mtx); // ロックの取得
++shared_resource; // 共有リソースのインクリメント
}
}
int main() {
const int num_threads = 10;
const int num_iterations = 1000000;
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_resource, num_iterations);
}
for (auto& th : threads) {
th.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Final resource value: " << shared_resource << std::endl;
std::cout << "Time taken: " << duration.count() << " seconds" << std::endl;
return 0;
}
このコードでは、10個のスレッドがそれぞれ100万回のインクリメント操作を行います。ロックの競合により、パフォーマンスに影響が出ることが分かります。
クリティカルセクションの短縮
ロックを保持する時間(クリティカルセクション)をできるだけ短くすることで、ロックの競合を減らし、パフォーマンスを向上させることができます。共有リソースに対する操作を最小限に抑え、他の計算や操作はロックの外で行うようにします。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
std::mutex mtx; // mutexの宣言
int shared_resource = 0;
void increment_resource(int iterations) {
int local_counter = 0;
for (int i = 0; i < iterations; ++i) {
++local_counter;
}
std::lock_guard<std::mutex> lock(mtx); // ロックの取得
shared_resource += local_counter; // 共有リソースのインクリメント
}
int main() {
const int num_threads = 10;
const int num_iterations = 1000000;
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_resource, num_iterations);
}
for (auto& th : threads) {
th.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Final resource value: " << shared_resource << std::endl;
std::cout << "Time taken: " << duration.count() << " seconds" << std::endl;
return 0;
}
ロックフリーのデータ構造
ロックフリーのデータ構造を使用することで、スレッド同期によるオーバーヘッドを削減できます。C++標準ライブラリには、std::atomicなどのロックフリーの同期機構が用意されています。
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <chrono>
std::atomic<int> shared_resource(0); // ロックフリーの共有リソース
void increment_resource(int iterations) {
for (int i = 0; i < iterations; ++i) {
++shared_resource; // ロックフリーのインクリメント
}
}
int main() {
const int num_threads = 10;
const int num_iterations = 1000000;
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_resource, num_iterations);
}
for (auto& th : threads) {
th.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Final resource value: " << shared_resource << std::endl;
std::cout << "Time taken: " << duration.count() << " seconds" << std::endl;
return 0;
}
この例では、std::atomicを使用することで、ロックによる待機時間を削減し、パフォーマンスを向上させています。
まとめ
スレッド同期はプログラムの正確性を保つために重要ですが、適切な手法を選択しないとパフォーマンスに悪影響を与えることがあります。クリティカルセクションを短く保つ、ロックフリーのデータ構造を使用するなどの工夫をすることで、効率的なスレッド同期を実現しましょう。
高度な同期方法
基本的なスレッド同期方法としてstd::mutexやstd::lock_guardを学びましたが、C++にはこれ以外にも高度な同期方法が提供されています。ここでは、std::unique_lockやstd::shared_mutexなどを使った同期方法を紹介します。
std::unique_lockの利用
std::unique_lockは、std::mutexのロック管理をより柔軟に行うためのクラスです。std::unique_lockを使うことで、ロックの遅延取得や早期解放が可能になります。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // mutexの宣言
void thread_function(int id) {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // ロックの遅延取得
// ここではまだロックされていない
// ロックを手動で取得
lock.lock();
std::cout << "Thread " << id << " acquired lock" << std::endl;
// ロックを手動で解放
lock.unlock();
std::cout << "Thread " << id << " released lock" << std::endl;
// 再度ロックを取得
lock.lock();
std::cout << "Thread " << id << " reacquired lock" << std::endl;
}
int main() {
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
t1.join();
t2.join();
return 0;
}
std::shared_mutexの利用
std::shared_mutexは、複数のスレッドが同時に読み取り操作を行うことを許可し、書き込み操作は単一のスレッドのみが行えるようにするための同期オブジェクトです。これにより、読み取り操作のパフォーマンスが向上します。
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex shared_mtx; // shared_mutexの宣言
int shared_data = 0;
void reader_function(int id) {
std::shared_lock<std::shared_mutex> lock(shared_mtx); // 共有ロックの取得
std::cout << "Reader " << id << " read data: " << shared_data << std::endl;
}
void writer_function(int id) {
std::unique_lock<std::shared_mutex> lock(shared_mtx); // 排他ロックの取得
shared_data += id;
std::cout << "Writer " << id << " wrote data: " << shared_data << std::endl;
}
int main() {
std::vector<std::thread> readers;
std::vector<std::thread> writers;
for (int i = 1; i <= 3; ++i) {
writers.emplace_back(writer_function, i);
}
for (int i = 1; i <= 5; ++i) {
readers.emplace_back(reader_function, i);
}
for (auto& writer : writers) {
writer.join();
}
for (auto& reader : readers) {
reader.join();
}
return 0;
}
条件変数の利用
条件変数を使うと、スレッドは特定の条件が満たされるまで待機することができます。条件変数はstd::condition_variableクラスとして提供されており、std::unique_lockとともに使用します。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
std::cout << "Thread " << id << std::endl;
}
void set_ready() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all(); // 全ての待機スレッドに通知
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_id, i);
}
std::thread notifier(set_ready);
for (auto& th : threads) {
th.join();
}
notifier.join();
return 0;
}
まとめ
C++には、std::mutexやstd::lock_guardに加えて、std::unique_lockやstd::shared_mutex、条件変数など、さまざまな同期機構が提供されています。これらを適切に利用することで、プログラムのパフォーマンスと安全性を向上させることができます。目的に応じて適切な同期手法を選び、効率的なスレッド同期を実現しましょう。
応用例: 複数リソースの管理
スレッド同期の技術は、単一のリソースに対するものだけでなく、複数のリソースを同時に管理する際にも重要です。ここでは、複数のリソースを効率的に管理するためのスレッド同期の応用例を紹介します。
複数のmutexを使用したリソース管理
複数のリソースを管理する際、各リソースに対して個別のmutexを用意し、必要に応じて同時に複数のロックを取得することがあります。以下は、複数のバッファを管理する例です。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx1, mtx2;
int buffer1 = 0, buffer2 = 0;
void transfer(int id, int amount) {
// ロックを取得する順序を統一する
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
buffer1 -= amount;
buffer2 += amount;
std::cout << "Thread " << id << " transferred " << amount << " from buffer1 to buffer2." << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 1; i <= 5; ++i) {
threads.emplace_back(transfer, i, 10 * i);
}
for (auto& th : threads) {
th.join();
}
std::cout << "Final buffer1 value: " << buffer1 << std::endl;
std::cout << "Final buffer2 value: " << buffer2 << std::endl;
return 0;
}
この例では、各スレッドがbuffer1からbuffer2に値を転送します。std::lockを使用することで、複数のmutexを安全にロックし、デッドロックを防止しています。
リーダー・ライター問題の解決
複数のスレッドが同時に読み取り操作を行い、書き込み操作は単一のスレッドのみが行えるようにするためには、リーダー・ライター問題を解決する必要があります。std::shared_mutexを使用することで、この問題を解決できます。
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex shared_mtx;
int shared_data = 0;
void reader_function(int id) {
std::shared_lock<std::shared_mutex> lock(shared_mtx); // 共有ロックの取得
std::cout << "Reader " << id << " read data: " << shared_data << std::endl;
}
void writer_function(int id) {
std::unique_lock<std::shared_mutex> lock(shared_mtx); // 排他ロックの取得
shared_data += id;
std::cout << "Writer " << id << " wrote data: " << shared_data << std::endl;
}
int main() {
std::vector<std::thread> readers;
std::vector<std::thread> writers;
for (int i = 1; i <= 3; ++i) {
writers.emplace_back(writer_function, i);
}
for (int i = 1; i <= 5; ++i) {
readers.emplace_back(reader_function, i);
}
for (auto& writer : writers) {
writer.join();
}
for (auto& reader : readers) {
reader.join();
}
return 0;
}
この例では、std::shared_mutexを使用して複数のリーダーが同時にデータを読み取り、単一のライターがデータを書き込むようにしています。これにより、リーダー・ライター問題を解決し、効率的なデータアクセスを実現しています。
条件変数を用いた生産者-消費者問題の解決
生産者-消費者問題では、生産者スレッドがデータを生成し、消費者スレッドがそのデータを消費します。条件変数を使用することで、スレッド間の通信と同期を効率的に行えます。
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::queue<int> buffer;
const unsigned int max_size = 10;
std::mutex mtx;
std::condition_variable cv;
void producer(int id) {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return buffer.size() < max_size; });
buffer.push(i);
std::cout << "Producer " << id << " produced " << i << std::endl;
cv.notify_all();
}
}
void consumer(int id) {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !buffer.empty(); });
int value = buffer.front();
buffer.pop();
std::cout << "Consumer " << id << " consumed " << value << std::endl;
cv.notify_all();
}
}
int main() {
std::thread p1(producer, 1);
std::thread p2(producer, 2);
std::thread c1(consumer, 1);
std::thread c2(consumer, 2);
p1.join();
p2.join();
c1.join();
c2.join();
return 0;
}
この例では、生産者スレッドがデータを生成し、消費者スレッドがそのデータを消費します。条件変数を使用することで、バッファのサイズに応じた適切な待機と通知を実現しています。
まとめ
複数のリソースを管理する際には、適切な同期手法を選択することが重要です。std::mutexやstd::shared_mutex、条件変数を使用することで、安全かつ効率的なリソース管理を実現しましょう。目的に応じた同期手法を適用することで、スレッド間の競合を最小限に抑え、プログラムのパフォーマンスを向上させることができます。
スレッド同期の演習問題
ここでは、スレッド同期に関する知識を確認し、理解を深めるための演習問題を提供します。各問題には、考慮すべきポイントや解決のヒントも含まれています。
問題1: 基本的なmutexの使用
以下のコードを修正して、スレッドが競合しないようにしてください。
#include <iostream>
#include <thread>
int counter = 0;
void increment_counter(int id) {
for (int i = 0; i < 1000; ++i) {
++counter;
}
std::cout << "Thread " << id << " finished.\n";
}
int main() {
std::thread t1(increment_counter, 1);
std::thread t2(increment_counter, 2);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
ヒント
std::mutexを使用して、counterへのアクセスを保護してください。
問題2: デッドロックの回避
以下のコードはデッドロックの危険性があります。この問題を解決してください。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread_function1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2);
std::cout << "Thread 1 finished.\n";
}
void thread_function2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(mtx1);
std::cout << "Thread 2 finished.\n";
}
int main() {
std::thread t1(thread_function1);
std::thread t2(thread_function2);
t1.join();
t2.join();
return 0;
}
ヒント
std::lockを使用して、複数のmutexを安全にロックしてください。
問題3: 条件変数を用いた生産者-消費者問題
以下のコードは、データが生成される前に消費される可能性があります。この問題を解決してください。
#include <iostream>
#include <thread>
#include <queue>
std::queue<int> buffer;
const unsigned int max_size = 10;
void producer() {
for (int i = 0; i < 20; ++i) {
buffer.push(i);
std::cout << "Produced " << i << std::endl;
}
}
void consumer() {
for (int i = 0; i < 20; ++i) {
if (!buffer.empty()) {
int value = buffer.front();
buffer.pop();
std::cout << "Consumed " << value << std::endl;
}
}
}
int main() {
std::thread p(producer);
std::thread c(consumer);
p.join();
c.join();
return 0;
}
ヒント
std::mutexとstd::condition_variableを使用して、生産者と消費者間の同期を実現してください。
問題4: std::shared_mutexの使用
以下のコードを修正して、リーダー・ライターロックを実装してください。複数のスレッドが同時に読み取りを行い、単一のスレッドが書き込みを行えるようにします。
#include <iostream>
#include <thread>
#include <vector>
int shared_data = 0;
void reader_function(int id) {
std::cout << "Reader " << id << " read data: " << shared_data << std::endl;
}
void writer_function(int id) {
shared_data += id;
std::cout << "Writer " << id << " wrote data: " << shared_data << std::endl;
}
int main() {
std::vector<std::thread> readers;
std::vector<std::thread> writers;
for (int i = 1; i <= 3; ++i) {
writers.emplace_back(writer_function, i);
}
for (int i = 1; i <= 5; ++i) {
readers.emplace_back(reader_function, i);
}
for (auto& writer : writers) {
writer.join();
}
for (auto& reader : readers) {
reader.join();
}
return 0;
}
ヒント
std::shared_mutex、std::shared_lock、およびstd::unique_lockを使用して、リーダー・ライターロックを実装してください。
まとめ
これらの演習問題を通じて、スレッド同期の基礎と応用についての理解を深めることができます。問題を解決することで、実際のプログラムでのスレッド同期の適用方法を習得しましょう。
まとめ
スレッド同期は、マルチスレッドプログラミングにおいて重要な技術です。C++の標準ライブラリが提供するstd::mutexやstd::lock_guard、std::unique_lock、std::shared_mutex、条件変数などを活用することで、安全かつ効率的にスレッド間のデータ競合を防ぐことができます。本記事では、基本的な使い方から高度な同期方法、応用例や演習問題までを紹介しました。これらの技術を適切に使いこなすことで、複雑なマルチスレッドプログラムでも高いパフォーマンスと安全性を維持することが可能になります。学んだ内容を実践に生かし、より堅牢なプログラムを開発しましょう。
コメント