@@ -16,6 +16,9 @@ These include:
16
16
* ` async_manual_reset_event `
17
17
* ` async_auto_reset_event `
18
18
* ` async_latch `
19
+ * ` sequence_barrier `
20
+ * ` multi_producer_sequencer `
21
+ * ` single_producer_sequencer `
19
22
* Functions
20
23
* ` sync_wait() `
21
24
* ` when_all() `
@@ -949,6 +952,262 @@ namespace cppcoro
949
952
}
950
953
```
951
954
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
+
952
1211
## ` cancellation_token `
953
1212
954
1213
A ` 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