C ++ STL生产者多个消费者,生产者在产生下一个价值之前等待免费消费者

时间:2018-04-02 13:06:01

标签: c++ multithreading c++11 asynchronous

我的小消费者 - 生产者问题让我感到困惑了一段时间。我不想要一个实现,其中一个生产者将一些数据循环推送给消费者,分别填满他们的数据队列。

我想拥有一个生产者,x个消费者,但生产者等待生产新数据,直到消费者再次获得免费。在我的示例中有3个消费者,因此生产者在任何给定时间最多创建3个数据对象。由于我不喜欢投票,消费者应该在完成后通知制作人。听起来很简单,但我找到的解决方案并不让我高兴。首先是代码。

#include "stdafx.h"
#include <mutex>
#include <iostream>
#include <future>
#include <map>
#include <atomic>

std::atomic_int totalconsumed;

class producer {
    using runningmap_t = std::map<int, std::pair<std::future<void>, bool>>;

    // Secure the map of futures.
    std::mutex mutex_;
    runningmap_t running_;

    // Used for finished notification
    std::mutex waitermutex_;
    std::condition_variable waiter_;

    // The magic number to limit the producer.
    std::atomic<int> count_;

    bool can_run();
    void clean();

    // Fake a source, e.g. filesystem scan.
    int fakeiter;
    int next();
    bool has_next() const;

public:
    producer() : fakeiter(50) {}
    void run();
    void notify(int value);
    void wait();
};

class consumer {
    producer& producer_;
public:
    consumer(producer& producer) : producer_(producer) {}
    void run(int value) {
        std::this_thread::sleep_for(std::chrono::milliseconds(42));
        std::cout << "Consumed " << value << " on (" << std::this_thread::get_id() << ")" << std::endl;
        totalconsumed++;
        producer_.notify(value);
    }
};


// Only if less than three threads are active, another gets to run.
bool producer::can_run() { return count_.load() < 3; }

// Verify if there's something to consume
bool producer::has_next() const { return 0 != fakeiter; }

// Produce the next value for consumption.
int producer::next() { return --fakeiter; }

// Remove the futures that have reported to be finished.
void producer::clean()
{
    for (auto it = running_.begin(); it != running_.end(); ) {
        if (it->second.second) {
            it = running_.erase(it);
        }
        else { 
            ++it;
        }
    }
}

// Runs the producer. Creates a new consumer for every produced value. Max 3 at a time.
void producer::run()
{
    while (has_next()) {
        if (can_run()) {
            auto c = next();

            count_++;
            auto future = std::async(&consumer::run, consumer(*this), c);

            std::unique_lock<std::mutex> lock(mutex_);
            running_[c] = std::make_pair(std::move(future), false);

            clean();
        }
        else {
            std::unique_lock<std::mutex> lock(waitermutex_);
            waiter_.wait(lock);
        }
    }
}

// Consumers diligently tell the producer that they are finished.
void producer::notify(int value)
{
    count_--;

    mutex_.lock();
    running_[value].second = true;
    mutex_.unlock();

    std::unique_lock<std::mutex> waiterlock(waitermutex_);
    waiter_.notify_all();
}

// Wait for all consumers to finish.
void producer::wait()
{
    while (!running_.empty()) {
        mutex_.lock();
        clean();
        mutex_.unlock();

        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

// Looks like the application entry point.
int main()
{
    producer p;

    std::thread pthread(&producer::run, &p);
    pthread.join();
    p.wait();

    std::cout << std::endl << std::endl << "Total consumed " << totalconsumed.load() << std::endl;

    return 0;
}

我不喜欢的部分是映射到期货的值列表,称为running_。我需要保持future,直到consumer实际完成。我无法从future方法中删除地图中的notify,否则我将终止当前正在调用notify的主题。

我是否遗漏了可以简化此构造的内容?

1 个答案:

答案 0 :(得分:0)

template<class T>
struct slotted_data {
  std::size_t I;
  T t;
};
template<class T>
using sink = std::function<void(T)>;
template<class T, std::size_t N>
struct async_slots {
  bool produce( slotted_data<T> data ) {
    if (terminate || data.I>=N) return false;
    {
      auto l = lock();
      if (slots[data.I]) return false;
      slots[data.I] = std::move(data.t);
    }
    cv.notify_one();
    return true;
  }
  // rare use of non-lambda cv.wait in the wild!
  bool consume(sink<slotted_data<T>> f) {
    auto l = lock();
    while(!terminate) {
      for (auto& slot:slots) {
        if (slot) {
          auto r = std::move(*slot);
          slot = std::nullopt;
          f({std::size_t(&slot-slots.data()), std::move(r)}); // invoke in lock
          return true;
        }
      }
      cv.wait(l);
    }
    return false;
  }
  // easier and safer version:
  std::optional<slotted_data<T>> consume() {
    std::optional<slotted_data<T>> r;
    bool worked = consume([&](auto&& data) { r = std::move(data); });
    if (!worked) return {};
    return r;
  }
  void finish() {
      {
        auto l = lock();
        terminate = true;
      }
      cv.notify_all();
  }
private:
  auto lock() { return std::unique_lock<std::mutex>(m); }
  std::mutex m;
  std::condition_variable cv;
  std::array< std::optional<T>, N > slots;
  bool terminate = false;
};

async_slots提供固定数量的广告位和等待消费。如果您尝试在同一个槽中生成两个东西,则生成器函数返回false并忽略您。

consume以连续传递方式调用互斥锁内的数据接收器。这允许原子消耗。

我们希望颠覆生产者和消费者:

template<class T, std::size_t N>
struct slotted_consumer {
  bool consume( std::size_t I, sink<T> sink ) {
    std::optional<T> data;
    std::condition_variable cv;
    std::mutex m;
    bool worked = slots.produce(
      {
        I,
        [&](auto&& t){
          {
            std::unique_lock<std::mutex> l(m);
            data.emplace(std::move(t));
          }
          cv.notify_one();
        }
      }
    );
    if (!worked) return false;
    std::unique_lock<std::mutex> l(m);
    cv.wait(l, [&]()->bool{
      return (bool)data;
    });
    sink( std::move(*data) );
    return true;
  }
  bool produce( T t ) {
    return slots.consume(
        [&](auto&& f) {
            f.t( std::move(t) );
        }
    );
  }
  void finish() {
      slots.finish();
  }
private:
  async_slots< sink<T>, N > slots;
};

在我们没有持有sink的互斥锁的情况下,我们必须小心谨慎地执行async_slots,这就是上面consume如此奇怪的原因。

Live example

您分享了slotted_consumer< int, 3 > slots。生产线程反复调用slots.produce(42);。它会阻塞,直到新的消费者排队。

消费者#2呼叫slots.consume( 2, [&](int x){ /* code to consume x */ } ),#1和#0也传递他们的插槽号码。

所有3位消费者都可以等待下一次生产。如果等待更多工作,上述系统默认先输入#0;我们可以以保持更多状态为代价使其“公平”。