C++と色々

主にC++やプログラムに関する記事を投稿します。

C++でProducer-Consumerパターン書いてみた

↓この本の第5章 Producer-ConsumerパターンをC++で書いてみた。

処理の流れとしては、ケーキを作る人(スレッド)が3人、ケーキを食べる人が3人、作ったケーキを置いておくテーブルの面積がケーキ3つ分、という風になっています。面倒くさいのでファイルは分割しませんでしたが、本のサンプルと対比はし易いかと思います。

#include <iostream>
#include <chrono>
#include <atomic>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <string>
#include <sstream>
#include <random>
#include <queue>

class table
{
  std::mutex mutex_;
  std::condition_variable condition_;

  std::queue<std::string> buffer_;
  const int count_;

public:
  explicit table(int count)
    : buffer_{}, count_{count}
  {
  }

  table(table const&) = delete;
  table& operator=(table const&) = delete;
  
  void push(std::string const& cake, std::string const& thread_name)
  {
    std::unique_lock<std::mutex> lock{mutex_};
    while (static_cast<int>(buffer_.size()) >= count_) {
      condition_.wait(lock);
    }
    buffer_.push(cake);
    condition_.notify_all();
    std::cout << thread_name << " puts " << cake << std::endl;
  }

  std::string pop(std::string const& thread_name)
  {
    std::unique_lock<std::mutex> lock{mutex_};
    while (buffer_.empty()) {
      condition_.wait(lock);
    }
    std::string cake = std::move(buffer_.front());
    buffer_.pop();
    condition_.notify_all();
    std::cout << thread_name << " takes " << cake << std::endl;
    return cake;
  }
};

class cake_maker
{
  table& table_;
  std::string name_;
  std::uniform_int_distribution<> rand_;
  std::mt19937 engine_;
  static std::atomic<int> id_;

public:
  cake_maker(table& table, std::string name, std::mt19937::result_type seed)
    : table_{table}, name_{std::move(name)}, rand_{0, 1000}, engine_{seed}
  {
  }

  void operator()()
  {
    while (true) {
      std::this_thread::sleep_for(std::chrono::milliseconds{rand_(engine_)});
      std::ostringstream oss;
      oss << "[cake no." << id_ << "] by " << name_;
      ++id_;
      table_.push(oss.str(), name_);
    }
  }
};

std::atomic<int> cake_maker::id_ = 0;

class cake_eater
{
  table& table_;
  std::string name_;
  std::uniform_int_distribution<> rand_;
  std::mt19937 engine_;

public:
  cake_eater(table& table, std::string name, std::mt19937::result_type seed)
    : table_{table}, name_{std::move(name)}, rand_{0, 1000}, engine_{seed}
  {
  }

  void operator()()
  {
    while (true) {
      std::string const& cake = table_.pop(name_);
      std::this_thread::sleep_for(std::chrono::milliseconds{rand_(engine_)});
    }
  }
};

int main()
{
  table table{10};
  std::thread maker1{cake_maker{table, "Maker-1", 31415}};
  std::thread maker2{cake_maker{table, "Maker-2", 92653}};
  std::thread maker3{cake_maker{table, "Maker-3", 58979}};
  std::thread eater1{cake_eater{table, "Eater-1", 32384}};
  std::thread eater2{cake_eater{table, "Eater-2", 62643}};
  std::thread eater3{cake_eater{table, "Eater-3", 38327}};
  maker1.join();
  maker2.join();
  maker3.join();
  eater1.join();
  eater2.join();
  eater3.join();
}

実行例

Maker-2 puts [cake no.0] by Maker-2
Eater-3 takes [cake no.0] by Maker-2
Maker-2 puts [cake no.1] by Maker-2
Eater-3 takes [cake no.1] by Maker-2
Maker-2 puts [cake no.2] by Maker-2
Eater-2 takes [cake no.2] by Maker-2
Maker-1 puts [cake no.3] by Maker-1
Eater-1 takes [cake no.3] by Maker-1
Maker-3 puts [cake no.4] by Maker-3
Eater-3 takes [cake no.4] by Maker-3
Maker-3 puts [cake no.5] by Maker-3
Eater-3 takes [cake no.5] by Maker-3
Maker-1 puts [cake no.6] by Maker-1
Maker-1 puts [cake no.7] by Maker-1
Eater-2 takes [cake no.6] by Maker-1
Eater-1 takes [cake no.7] by Maker-1
Maker-3 puts [cake no.8] by Maker-3
Maker-2 puts [cake no.9] by Maker-2
^C続行するには何かキーを押してください . . .

C++標準でスレッドに名前をつける方法がなかったので、苦肉の策として、pushpopの引数にスレッド名を渡す引数を追加しました。

while (static_cast<int>(buffer_.size()) >= count_) {
  condition_.wait(lock);
}

while (buffer_.empty()) {
  condition_.wait(lock);
}

condition_.wait(lock, [this] {
  return static_cast<int>(buffer_.size()) < count_;
});

condition_.wait(lock, [this] {
  return !buffer_.empty();
})

とも書けますが、本との比較を意識して、whileで書きました。

https://sites.google.com/site/cpprefjp/article/how_to_use_cv にはnot_empty_not_full_の2つの条件変数に分かれていますが、何故なんでしょうか? 1つでも正しく動いてると思いますが、そこが分からなかったです。

(追記こちらを参照

条件変数 Step-by-Step入門 - yohhoyの日記(別館)

)

このプログラムは無限ループなので、Ctrl + Cなどで強制終了して下さい。