1 /* Copyright (c) 2021. The SimGrid Team. All rights reserved. */
3 /* This program is free software; you can redistribute it and/or modify it
4 * under the terms of the license (GNU LGPL) which comes with this package. */
6 #ifndef SIMGRID_PLUGIN_PRODUCERCONSUMER_HPP
7 #define SIMGRID_PLUGIN_PRODUCERCONSUMER_HPP
9 #include <simgrid/s4u/ConditionVariable.hpp>
10 #include <simgrid/s4u/Mailbox.hpp>
11 #include <simgrid/s4u/Mutex.hpp>
12 #include <xbt/asserts.h>
20 XBT_LOG_NEW_CATEGORY(producer_consumer, "Producer-Consumer plugin logging category");
22 /** Stock implementation of a generic monitored queue to solve the producer-consumer problem */
27 template <typename T> class ProducerConsumer;
28 template <typename T> using ProducerConsumerPtr = boost::intrusive_ptr<ProducerConsumer<T>>;
30 static unsigned long pc_id = 0;
32 template <typename T> class ProducerConsumer {
34 /** This ProducerConsumer plugin can use two different transfer modes:
35 * - TransferMode::MAILBOX: this mode induces a s4u::Comm between the actors doing the calls to put() and get().
36 * If these actors are on the same host, this communication goes through the host's loopback and can thus be
37 * seen as a memory copy. Otherwise, data goes over the network.
38 * - TransferMode::QUEUE: data is internally stored in a std::queue. Putting and getting data to and from this
39 * data structure has a zero-cost in terms of simulated time.
40 * Both modes guarantee that the data is consumed in the order it has been produced. However, when data goes
41 * through the network, s4u::Comm are started in the right order, but may complete in a different order depending
42 * the characteristics of the different interconnections between host pairs.
44 enum class TransferMode { MAILBOX = 0, QUEUE };
49 /* Implementation of a Monitor to handle the data exchanges */
51 s4u::ConditionVariablePtr can_put_;
52 s4u::ConditionVariablePtr can_get_;
54 /* data containers for each of the transfer modes */
55 s4u::Mailbox* mbox_ = nullptr;
56 std::queue<T*> queue_;
58 unsigned int max_queue_size_ = 1;
59 TransferMode tmode_ = TransferMode::MAILBOX;
61 /* Refcounting management */
62 std::atomic_int_fast32_t refcount_{0};
63 friend void intrusive_ptr_add_ref(ProducerConsumer* pc) { pc->refcount_.fetch_add(1, std::memory_order_acq_rel); }
65 friend void intrusive_ptr_release(ProducerConsumer* pc)
67 if (pc->refcount_.fetch_sub(1, std::memory_order_release) == 1) {
68 std::atomic_thread_fence(std::memory_order_acquire);
73 ProducerConsumer(unsigned int max_queue_size) : max_queue_size_(max_queue_size)
75 xbt_assert(max_queue_size > 0, "Max queue size of 0 is not allowed");
77 id = std::string("ProducerConsumer") + std::to_string(pc_id);
80 mutex_ = s4u::Mutex::create();
81 can_put_ = s4u::ConditionVariable::create();
82 can_get_ = s4u::ConditionVariable::create();
84 if (tmode_ == TransferMode::MAILBOX)
85 mbox_ = s4u::Mailbox::by_name(id);
87 ~ProducerConsumer() = default;
90 /** Creation of the monitored queue. Its size can be bounded by passing a strictly positive value to 'max_queue_size'
91 * as parameter. Calling 'create()' means that the queue size is (virtually) infinite.
93 static ProducerConsumerPtr<T> create(unsigned int max_queue_size = UINT_MAX)
95 return ProducerConsumerPtr<T>(new ProducerConsumer<T>(max_queue_size));
98 /** This method is intended more to set the maximum queue size in a fluent way than changing the size during the
99 * utilization of the ProducerConsumer. Hence, the modification occurs in a critical section to prevent
102 ProducerConsumer* set_max_queue_size(unsigned int max_queue_size)
104 std::unique_lock<s4u::Mutex> lock(*mutex_);
105 max_queue_size_ = max_queue_size;
109 unsigned int get_max_queue_size() { return max_queue_size_; }
111 /** The underlying data container (and transfer mode) can only be modified when the queue is empty.*/
112 ProducerConsumer* set_transfer_mode(TransferMode new_mode)
114 if (tmode_ == new_mode) /* No change, do nothing */
117 xbt_assert(empty(), "cannot change transfer mode when some data is in queue");
118 if (new_mode == TransferMode::MAILBOX) {
119 mbox_ = s4u::Mailbox::by_name(id);
126 std::string get_transfer_mode() { return tmode_ == TransferMode::MAILBOX ? "mailbox" : "queue"; }
128 /** Container-agnostic size() method */
129 unsigned int size() { return tmode_ == TransferMode::MAILBOX ? mbox_->size() : queue_.size(); }
131 /** Container-agnostic empty() method */
132 bool empty() { return tmode_ == TransferMode::MAILBOX ? mbox_->empty() : queue_.empty(); }
134 /** Asynchronous put() of a data item of a given size
135 * - TransferMode::MAILBOX: if put_async is called directly from user code, it can be considered to be done in a
136 * fire-and-forget mode. No need to save the s4u::CommPtr.
137 * - TransferMode::QUEUE: the data is simply pushed into the queue.
139 s4u::CommPtr put_async(T* data, size_t simulated_size_in_bytes)
141 std::unique_lock<s4u::Mutex> lock(*mutex_);
142 s4u::CommPtr comm = nullptr;
143 XBT_CVERB(producer_consumer, (size() < max_queue_size_) ? "can put" : "must wait");
145 while (size() >= max_queue_size_)
146 can_put_->wait(lock);
147 if (tmode_ == TransferMode::MAILBOX) {
148 comm = mbox_->put_async(data, simulated_size_in_bytes);
151 can_get_->notify_all();
155 /** Synchronous put() of a data item of a given size
156 * - TransferMode::MAILBOX: the caller must wait for the induced communication with the getter of the data to be
157 * complete to continue with its execution. This wait is done outside of the monitor to prevent serialization.
158 * - TransferMode::QUEUE: the behavior is exactly the same as put_async: data is simply pushed into the queue.
160 void put(T* data, size_t simulated_size_in_bytes)
162 s4u::CommPtr comm = put_async(data, simulated_size_in_bytes);
164 XBT_CDEBUG(producer_consumer, "Waiting for the data to be consumed");
169 /** Asynchronous get() of a 'data'
170 * - TransferMode::MAILBOX: the caller is returned a s4u::CommPtr onto which it can wait when the data is really
172 * - TransferMode::QUEUE: the data is simply popped from the queue and directly available. Better to call get() in
173 * this transfer mode.
175 s4u::CommPtr get_async(T** data)
177 std::unique_lock<s4u::Mutex> lock(*mutex_);
178 s4u::CommPtr comm = nullptr;
179 XBT_CVERB(producer_consumer, empty() ? "must wait" : "can get");
181 can_get_->wait(lock);
182 if (tmode_ == TransferMode::MAILBOX)
183 comm = mbox_->get_async<T>(data);
185 *data = queue_.front();
188 can_put_->notify_all();
193 /** Synchronous get() of a 'data'
194 * - TransferMode::MAILBOX: the caller waits (outside the monitor to prevent serialization) for the induced
195 * communication to be complete to continue with its execution.
196 * - TransferMode::QUEUE: the behavior is exactly the same as get_async: data is simply popped from the queue and
197 * directly available to the caller.
202 s4u::CommPtr comm = get_async(&data);
204 XBT_CDEBUG(producer_consumer, "Waiting for the data to arrive");
207 XBT_CDEBUG(producer_consumer, "data is available");
212 } // namespace plugin
213 } // namespace simgrid
215 #endif // SIMGRID_PLUGIN_PRODUCERCONSUMER_HPP