@@ -16,6 +16,9 @@ These include:
1616 * ` async_manual_reset_event `
1717 * ` async_auto_reset_event `
1818 * ` async_latch `
19+ * ` sequence_barrier `
20+ * ` multi_producer_sequencer `
21+ * ` single_producer_sequencer `
1922* Functions
2023 * ` sync_wait() `
2124 * ` when_all() `
@@ -949,6 +952,262 @@ namespace cppcoro
949952}
950953```
951954
955+ ## `sequence_barrier`
956+
957+ A `sequence_barrier` is a synchronisation primitive that allows a single-producer
958+ and multiple consumers to coordinate with respect to a monotonically increasing
959+ sequence number.
960+
961+ A single producer advances the sequence number by publishing new sequence numbers
962+ in a monotonically increasing order. One or more consumers can query the last
963+ published sequence number and can wait until a particular sequence number has been
964+ published.
965+
966+ A sequence barrier can be used to represent a cursor into a thread-safe producer/consumer
967+ ring-buffer
968+
969+ See the LMAX Disruptor pattern for more background:
970+ https://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf
971+
972+ API Synopsis:
973+ ```c++
974+ namespace cppcoro
975+ {
976+ template<typename SEQUENCE = std::size_t,
977+ typename TRAITS = sequence_traits<SEQUENCE>>
978+ class sequence_barrier
979+ {
980+ public:
981+ sequence_barrier(SEQUENCE initialSequence = TRAITS::initial_sequence) noexcept;
982+ ~sequence_barrier();
983+
984+ SEQUENCE last_published() const noexcept;
985+
986+ // Wait until the specified targetSequence number has been published.
987+ //
988+ // If the operation does not complete synchonously then the awaiting
989+ // coroutine is resumed on the specified scheduler. Otherwise, the
990+ // coroutine continues without suspending.
991+ //
992+ // The co_await expression resumes with the updated last_published()
993+ // value, which is guaranteed to be at least 'targetSequence'.
994+ template<typename SCHEDULER>
995+ [[nodiscard]]
996+ Awaitable<SEQUENCE> wait_until_published(SEQUENCE targetSequence,
997+ SCHEDULER& scheduler) const noexcept;
998+
999+ void publish(SEQUENCE sequence) noexcept;
1000+ };
1001+ }
1002+ ```
1003+
1004+ ## ` single_producer_sequencer `
1005+
1006+ A ` single_producer_sequencer ` is a synchronisation primitived that can be used to
1007+ coordinate access to a ring-buffer for a single producer and one or more consumers.
1008+
1009+ A producer first acquires one or more slots in a ring-buffer, writes to the ring-buffer
1010+ elements corresponding to those slots, and then finally publishes the values written to
1011+ those slots. A producer can never produce more than 'bufferSize' elements in advance
1012+ of where the consumer has consumed up to.
1013+
1014+ A consumer then waits for certain elements to be published, processes the items and
1015+ then notifies the producer when it has finished processing items by publishing the
1016+ sequence number it has finished consuming in a ` sequence_barrier ` object.
1017+
1018+
1019+ API Synopsis:
1020+ ```
1021+ // <cppcoro/single_producer_sequencer.hpp>
1022+ namespace cppcoro
1023+ {
1024+ template<
1025+ typename SEQUENCE = std::size_t,
1026+ typename TRAITS = sequence_traits<SEQUENCE>>
1027+ class single_producer_sequencer
1028+ {
1029+ public:
1030+ using size_type = typename sequence_range<SEQUENCE, TRAITS>::size_type;
1031+
1032+ single_producer_sequencer(
1033+ const sequence_barrier<SEQUENCE, TRAITS>& consumerBarrier,
1034+ std::size_t bufferSize,
1035+ SEQUENCE initialSequence = TRAITS::initial_sequence) noexcept;
1036+
1037+ // Publisher API:
1038+
1039+ template<typename SCHEDULER>
1040+ [[nodiscard]]
1041+ Awaitable<SEQUENCE> claim_one(SCHEDULER& scheduler) noexcept;
1042+
1043+ template<typename SCHEDULER>
1044+ [[nodiscard]]
1045+ Awaitable<sequence_range<SEQUENCE>> claim_up_to(
1046+ std::size_t count,
1047+ SCHEDULER& scheduler) noexcept;
1048+
1049+ void publish(SEQUENCE sequence) noexcept;
1050+
1051+ // Consumer API:
1052+
1053+ SEQUENCE last_published() const noexcept;
1054+
1055+ template<typename SCHEDULER>
1056+ [[nodsicard]]
1057+ Awaitable<SEQUENCE> wait_until_published(
1058+ SEQUENCE targetSequence,
1059+ SCHEDULER& scheduler) const noexcept;
1060+
1061+ };
1062+ }
1063+ ```
1064+
1065+ Example usage:
1066+ ``` c++
1067+ using namespace cppcoro ;
1068+ using namespace std ::chrono;
1069+
1070+ struct message
1071+ {
1072+ int id;
1073+ steady_clock::time_point timestamp;
1074+ float data;
1075+ };
1076+
1077+ constexpr size_t bufferSize = 16384 ; // Must be power-of-two
1078+ constexpr size_t indexMask = bufferSize - 1 ;
1079+ message buffer[bufferSize];
1080+
1081+ task<void > producer (
1082+ io_service& ioSvc,
1083+ single_producer_sequencer<size_t>& sequencer)
1084+ {
1085+ auto start = steady_clock::now();
1086+ for (int i = 0; i < 1'000'000; ++i)
1087+ {
1088+ // Wait until a slot is free in the buffer.
1089+ size_t seq = co_await sequencer.claim_one(ioSvc);
1090+
1091+ // Populate the message.
1092+ auto& msg = buffer[seq & indexMask];
1093+ msg.id = i;
1094+ msg.timestammp = steady_clock::now();
1095+ msg.data = s;
1096+
1097+ // Publish the message.
1098+ sequencer.publish(seq);
1099+ }
1100+
1101+ // Publish a sentinel
1102+ auto seq = co_await sequencer.claim_one(ioSvc);
1103+ auto& msg = buffer[ seq & indexMask] ;
1104+ msg.id = -1;
1105+ sequencer.publish(seq);
1106+ }
1107+
1108+ task<void > consumer(
1109+ static_thread_pool& threadPool,
1110+ const single_producer_sequencer<size_t>& sequencer,
1111+ consumer_barrier<size_t>& consumerBarrier)
1112+ {
1113+ size_t nextToRead = 0;
1114+ while (true)
1115+ {
1116+ // Wait until the next message is available
1117+ // There may be more than one available.
1118+ const size_t available = co_await sequencer.wait_until_published(nextToRead, threadPool);
1119+ do {
1120+ auto& msg = buffer[ nextToRead & indexMask] ;
1121+ if (msg.id == -1)
1122+ {
1123+ consumerBarrier.publish(nextToRead);
1124+ co_return;
1125+ }
1126+
1127+ processMessage(msg);
1128+ } while (nextToRead++ != available);
1129+
1130+ // Notify the producer that we've finished processing
1131+ // up to 'nextToRead - 1'.
1132+ consumerBarrier.publish(available);
1133+ }
1134+ }
1135+
1136+ task<void > example(io_service& ioSvc, static_thread_pool& threadPool)
1137+ {
1138+ consumer_barrier<size_t> barrier;
1139+ single_producer_sequencer<size_t> sequencer{barrier, bufferSize};
1140+
1141+ co_await when_all(
1142+ publisher(ioSvc, sequencer),
1143+ consumer(threadPool, sequencer, barrier));
1144+ }
1145+ ```
1146+
1147+ ## `multi_producer_sequencer`
1148+
1149+ The `multi_producer_sequencer` class is a synchronisation primitive that coordinates
1150+ access to a ring-buffer for multiple producers and one or more consumers.
1151+
1152+ For a single-producer variant see the `single_producer_sequencer` class.
1153+
1154+ Note that the ring-buffer must have a size that is a power-of-two. This is because
1155+ the implementation uses bitmasks instead of integer division/modulo to calculate
1156+ the offset into the buffer. Also, this allows the sequence number to safely wrap
1157+ around the 32-bit/64-bit value.
1158+
1159+ API Summary:
1160+ ```c++
1161+ // <cppcoro/multi_producer_sequencer.hpp>
1162+ namespace cppcoro
1163+ {
1164+ template<typename SEQUENCE = std::size_t,
1165+ typename TRAITS = sequence_traits<SEQUENCE>>
1166+ class multi_producer_sequencer
1167+ {
1168+ public:
1169+ multi_producer_sequencer(
1170+ const sequence_barrier<SEQUENCE, TRAITS>& consumerBarrier,
1171+ SEQUENCE initialSequence = TRAITS::initial_sequence);
1172+
1173+ std::size_t buffer_size() const noexcept;
1174+
1175+ // Consumer interface
1176+ //
1177+ // Each consumer keeps track of their own 'lastKnownPublished' value
1178+ // and must pass this to the methods that query for an updated last-known
1179+ // published sequence number.
1180+
1181+ SEQUENCE last_published_after(SEQUENCE lastKnownPublished) const noexcept;
1182+
1183+ template<typename SCHEDULER>
1184+ Awaitable<SEQUENCE> wait_until_published(
1185+ SEQUENCE targetSequence,
1186+ SEQUENCE lastKnownPublished,
1187+ SCHEDULER& scheduler) const noexcept;
1188+
1189+ // Producer interface
1190+
1191+ // Query whether any slots available for claiming (approx.)
1192+ bool any_available() const noexcept;
1193+
1194+ template<typename SCHEDULER>
1195+ Awaitable<SEQUENCE> claim_one(SCHEDULER& scheduler) noexcept;
1196+
1197+ template<typename SCHEDULER>
1198+ Awaitable<sequence_range<SEQUENCE, TRAITS>> claim_up_to(
1199+ std::size_t count,
1200+ SCHEDULER& scheduler) noexcept;
1201+
1202+ // Mark the specified sequence number as published.
1203+ void publish(SEQUENCE sequence) noexcept;
1204+
1205+ // Mark all sequence numbers in the specified range as published.
1206+ void publish(const sequence_range<SEQUENCE, TRAITS>& range) noexcept;
1207+ };
1208+ }
1209+ ```
1210+
9521211## ` cancellation_token `
9531212
9541213A ` cancellation_token ` is a value that can be passed to a function that allows the caller to subsequently communicate a request to cancel the operation to that function.
0 commit comments