Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 17bd85a

Browse files
authored
Merge pull request lewissbaker#92 from lewissbaker/disruptor
Add synchronisation primitives for ring-buffered producer/consumer
2 parents e0d0324 + a6ac2ec commit 17bd85a

14 files changed

+2612
-0
lines changed

README.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

9541213
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.

include/cppcoro/config.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,11 @@
143143
# define CPPCORO_CPU_64BIT 0
144144
#endif
145145

146+
#if CPPCORO_COMPILER_MSVC
147+
# define CPPCORO_CPU_CACHE_LINE std::hardware_destructive_interference_size
148+
#else
149+
// On most architectures we can assume a 64-byte cache line.
150+
# define CPPCORO_CPU_CACHE_LINE 64
151+
#endif
152+
146153
#endif

0 commit comments

Comments
 (0)