メインコンテンツへスキップ

std::recursive_mutexを使う

·1067 文字·
技術解説 C++ STL
komori-n
著者
komori-n

知らなかったのでメモ。

以下のように、コールバック関数を登録したり呼び出したりできるクラス Hoge を考える。

#include <iostream>
#include <cstdlib>
#include <functional>

class Hoge {
public:
  template <typename F>
  void set(F&& f) {
    callback_ = std::forward<F>(f);
  }

  void clear(void) {
    callback_ = nullptr;
  }

  void invoke(void) {
    if (callback_) {
      callback_();
    }
  }

private:
  std::function<void(void)> callback_;
};

int main(int argc, char*argv[]) {
  Hoge hoge;

  hoge.set([](void) { std::cout << "hello from callback" << std::endl; });
  hoge.invoke();
  // => hello from callback
  hoge.clear();
  hoge.invoke();
  // => (none)

  return EXIT_SUCCESS;
}

Hoge::set()で呼んで欲しい関数を登録し、 Hoge::invoke()で登録された関数があれば呼び出すことができる。関数が登録されていなければ、何も行わない。また、 Hoge::clear()により関数の登録を解除することもできる。

このクラス Hoge をmultithreadに対応させたい。すなわち、複数スレッドが同時に set()invoke() を呼んでもうまく排他できるようにしたい。

シンプルに考えると、以下のように std::mutex でlockをとればいい気がする。

class Hoge {
public:
  template <typename F>
  void set(F&& f) {
    std::lock_guard<std::mutex> lock(mutex_);
    callback_ = std::forward<F>(f);
  }

  void clear(void) {
    std::lock_guard<std::mutex> lock(mutex_);
    callback_ = nullptr;
  }

  void invoke(void) {
    std::lock_guard<std::mutex> lock(mutex_);
    if (callback_) {
      callback_();
    }
  }

private:
  std::mutex mutex_;
  std::function<void(void)> callback_;
};

しかし、 std::mutex を使うこの方法は一つ欠点がある。それは、Hoge::invoke()のコールバック中に Hoge::clear()を呼び出すとデッドロックになってしまうことである1

  Hoge hoge;

  hoge.set([&](void) {
    std::cout << "hello from callback" << std::endl;
    hoge.clear();  // dead lock!!
  });
  hoge.invoke();

これを避けるためには、Hoge::invoke()経由で呼ばれたかどうかをクラス内部で覚えておく必要があると思っていた。

これは、C++標準ライブラリに入っているstd::recursive_mutexを使えば解決できる。std::recursive_mutexは(名前の通り)再帰関数用の排他変数で、同じスレッドから複数回 lock()がくると内部のカウンタをインクリメントし、unlock()がくるとデクリメントする。そして、unlock()後に内部カウンタが0になった場合のみロックを解除するという動作になっている。

#include <iostream>
#include <cstdlib>
#include <mutex>
#include <functional>
#include <thread>

class Hoge {
public:
  template <typename F>
  void set(F&& f) {
    std::lock_guard<std::recursive_mutex> lock(mutex_);
    callback_ = std::forward<F>(f);
  }

  void clear(void) {
    std::lock_guard<std::recursive_mutex> lock(mutex_);
    callback_ = nullptr;
  }

  void invoke(void) {
    std::lock_guard<std::recursive_mutex> lock(mutex_);
    if (callback_) {
      callback_();
    }
  }

private:
  std::recursive_mutex mutex_;
  std::function<void(void)> callback_;
};

int main(int argc, char*argv[]) {
  Hoge hoge;

  hoge.set([&](void) {
    std::cout << "hello from callback" << std::endl;
    hoge.clear();  // dead lockせずちゃんと clear できる
  });
  hoge.invoke();
  // => hello from callback

  hoge.invoke();
  // => (none)

  return EXIT_SUCCESS;
}

std::recursive_mutexを用いれば、所望の通りcallback内でclear()を呼んでもdead lockが起こらなくなった。

C++にはまだまだ知らない機能がいっぱいあって怖い。


  1. 厳密には、同じスレッドから同じmutexをロックしようとしたときの動作はundefined。手元の環境では、-lpthreadをつけてビルドしたらデッドロックになったが、つけずにビルドしたら普通に実行できた。 ↩︎

Related

std::functionやunique_functionを用いて、std::futureを中継する
·4036 文字
技術解説 C++ STL
SFINAEでtemplate classのメンバ関数の実体化を制御する
·1734 文字
技術解説 C++ SFINAE STL
libstdc++のstd::functionの実装を眺める
·3220 文字
技術解説 C++ STL