ロック - 排他処理

共通のデータを扱う複数のスレッドを起動する場合には排他処理のためのロックが必須となります。あるスレッドがロックをかけると他のスレッドがロックをかけようとしてもロックが解除されるまで待たされます。本当は「待たされる」だけでなくいろいろな挙動をさせることもできますが、複雑になるので「待たされる」という挙動のみをここでは扱います。

class Mutex {
public:
  Mutex();
  ~Mutex();
  void lock();
  void unlock();
  void wait();
  void signal();
  void broadcast();
protected:
  pthread_mutex_t       mutex;
  pthread_cond_t        condition;
};

Mutex::Mutex() {
  int stat;
  if ((stat = pthread_mutex_init(&mutex, NULL)) != 0) {
    throw Exception(strerror(stat));
  }
  if ((stat = pthread_cond_init(&condition, NULL)) != 0) {
    pthread_mutex_destroy(&mutex);
    throw Exception(strerror(stat));
  }
}
Mutex::~Mutex() {
  pthread_mutex_destroy(&mutex);
  pthread_cond_destroy(&condition);
}
void
Mutex::lock() {
  int stat;
  if ((stat = pthread_mutex_lock(&mutex)) != 0) {
    throw Exception(strerror(stat));
  }
}
void
Mutex::unlock() {
  int stat;
  if ((stat = pthread_mutex_unlock(&mutex)) != 0) {
    throw Exception(strerror(stat));
  }
}
void
Mutex::signal() {
  int stat;
  if ((stat = pthread_cond_signal(&condition)) != 0) {
    throw Exception(strerror(stat));
  }
}
void
Mutex::wait() {
  int stat;
  if ((stat = pthread_cond_wait(&condition, &mutex)) != 0) {
    throw Exception(strerror(stat));
  }
}
void
Mutex::broadcast() {
  int stat;
  if ((stat = pthread_cond_broadcast(&condition)) != 0) {
    throw Exception(strerror(stat));
  }
}

以上のようにpthreadを使って排他処理を記述しています。まぁ、pthreadにC++をかぶせているだけですね。

lock()、unlock()でロックをかけたり解除したりします。他のメソッドは次のスレッドプールの実装のために使うので、ここでは気にしないで下さい。

では使い方のサンプルですが、ロックがないとうまくいかない例です。

class MySimpleThread : public Thread {
public:
  MySimpleThread(int &c, Mutex &m):
    count(c), mutex(m) {}
  ~MySimpleThread() {}
  void run();
  int 
  Mutex 
};

void
MySimpleThread::run()
{
  for (int i = 0; i < 10; i++) {
    int c = count;
    usleep(1);
    count = c + 1;
  }
}

int
main()
{
  int count = 0;
  Mutex mutex;
  MySimpleThread thread1(count, mutex), thread2(count, mutex);
  thread1.start();
  thread2.start();

  thread1.join();
  thread2.join();
  cout << "count=" << count << endl;
}

二つのスレッドが共通のcountという変数をカウントアップしています。すれぞれ10回カウントアップしているので最終的には20になってほしいのですが、動作させると

count=12

といった具合に20未満の数字になると思います。しかも、動作させるたびに違う値になったりします。共有するデータを同時に更新することによる問題です。そこで次のようにスレッドの処理を変更します。データの更新中にロックをかけます。

void
MySimpleThread::run()
{
  for (int i = 0; i < 10; i++) {
    mutex.lock();
    int c = count;
    usleep(1);
    count = c + 1;
    mutex.unlock();
  }
}

これを実行すると

count=20

と、正しく表示されます。

ロックは実に便利な仕組みですが安易に使うとデッドロックを起こしたりして、実に奥が深い機能です。また、排他処理をすればするほど並列処理のメリットが減少するのでなるべく使わないで済むロジックを考えるのが重要です。

前回:C++簡単スレッドプログラミング

次回:スレッドプール