Skip to content

Commit d9f721b

Browse files
Merge pull request #10 from connectivecpp/develop
Merge develop to main
2 parents 8540ec7 + 18c254e commit d9f721b

7 files changed

Lines changed: 393 additions & 15 deletions

File tree

‎.github/workflows/build_run_unit_test_cmake.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: create-build-dir
2525
run: mkdir build
2626
- name: configure-cmake
27-
run: cd build && cmake -D WAIT_QUEUE_BUILD_TESTS:BOOL=ON -D JM_CIRCULAR_BUFFER_BUILD_TESTS:BOOL=OFF ..
27+
run: cd build && cmake -D WAIT_QUEUE_BUILD_TESTS:BOOL=ON -D WAIT_QUEUE_BUILD_EXAMPLES:BOOL=ON -D JM_CIRCULAR_BUFFER_BUILD_TESTS:BOOL=OFF ..
2828
- name: build
2929
run: cd build && cmake --build . --config $BUILD_TYPE
3030
- name: run-unit-test

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
![GH Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/connectivecpp/wait-queue/gen_docs.yml?branch=main&label=GH%20Actions%20generate%20docs)
1010

11+
![GH Tag](https://img.shields.io/github/v/tag/connectivecpp/wait-queue?label=GH%20tag)
12+
1113
## Overview
1214

1315
`wait_queue` is a multi-reader, multi-writer FIFO thread-safe wait queue (often called MPMC for multiple producer / multiple consumer) for transferring data between threads. It is templatized on the type of data passed through the queue as well as the queue container type. Data is passed with value semantics, either by copying or by moving (as opposed to a queue that transfers data by pointer or reference). The wait queue has both wait and no-wait pop semantics. A fixed size container (e.g. a `ring_span`) can be used, eliminating any and all dynamic memory management (useful in embedded or deterministic environments). Similarly, a circular buffer that only allocates on construction can be used, which eliminates dynamic memory management when pushing or popping values on or off the queue.
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
/** @file
2+
*
3+
* @brief Multithreaded demo of @c chops::shared_buffer and @c chops::wait_queue.
4+
*
5+
* @author Thurman Gillespy
6+
*
7+
* Copyright (c)2019 by Thurman Gillespy
8+
* 3/22/19
9+
*
10+
* Distributed under the Boost Software License, Version 1.0.
11+
* (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
12+
*
13+
* Sample make file:
14+
* g++ -std=c++17 -I ~/Projects/utility-rack/include/ multithreaded_demo.cpp -lpthread
15+
*/
16+
17+
#include <iostream>
18+
#include <cstdlib> // std::srand, rand; EXIT_SUCCESS
19+
#include <vector>
20+
#include <string>
21+
#include <thread> // std::thread
22+
#include <functional> // std::mem_fn
23+
#include <algorithm> // std::for_each
24+
#include <atomic> // std::atomic
25+
#include <future> // std::promise, std::future
26+
#include <ctime> // std::time
27+
28+
#include "queue/wait_queue.hpp"
29+
#include "marshall/shared_buffer.hpp"
30+
31+
32+
/** Project Overview
33+
*
34+
* This demo program shows how to use @c chops::wait_queue and @c chops::shared_buffer
35+
* in a multithreaded environment. The program simulates multiple peripheral data
36+
* generators, perhaps sensors or network connections. THe data is handled by one or
37+
* more data processors, which sorts and formats the data, and periodically sends it to
38+
* a simulated database.
39+
*
40+
* The program can have 1 or more DeviceDataGenerator threads that each put 20 random
41+
* numbers into device_q, a @c chops::wait_queue. Over 1000 threads can be run sucessfully
42+
* (default is 20). Each DeviceDataGenerator thread generates random numbers in its' own
43+
* 'centile': thread 0: <0..99>; thread 1: <100..199>; thread 2: <200-299>, etc.
44+
*
45+
* The device_q numbers are read by 1 or more (default is 5) DataProcessor threads.
46+
* The numbers are sorted by centile. When 5 numbers in the same centile are collected,
47+
* a string with the numbers is created that is placed into data_q, another wait_queue
48+
* of type @c chops::wait_queue<chops::const_shared_buffer>>, ie, the string is copied into
49+
* a new @c chops::const_shared_buffer in the data_q. The first number in the string is the
50+
* 'index' that correspons to that centile, and the remaining numbers are kept in
51+
* chronologic order.
52+
*
53+
* 0 87 17 65 5 32s
54+
* 8 870 813 808 827 874
55+
*
56+
* The data_q is read by the single Database thread. The string is extracted from the
57+
* data_q and appened to the proper centile string in the database.
58+
*
59+
* When the Database thread is finished, a 'Data Report' is printed. Each row contains the
60+
* random nubmers created by a particular thread, in chronlogic order.
61+
*
62+
* Data Report
63+
* [0] 30 71 2 99 60 74 11 70 4 41 83 90 14 72 68 68 88 51 28 78
64+
* [1] 103 110 186 136 152 187 165 169 108 157 116 138 136 132 153 144 159 144 120 110
65+
* [2] 273 215 249 265 218 252 286 239 273 269 242 263 245 249 215 266 214 245 258 214
66+
* [3] 387 303 312 350 323 331 343 374 363 371 390 335 367 317 397 341 394 399 341 366
67+
* [4] 447 415 448 464 455 489 462 446 440 452 417 426 417 446 427 433 484 431 445 468
68+
*
69+
* Mutex locks and conditional variable signaling is handled within the
70+
* @c chops::wait_queue class.
71+
*
72+
*/
73+
74+
// @c wait_queue for moving data between threads
75+
using device_q_t = chops::wait_queue<int>;
76+
using data_q_t = chops::wait_queue<chops::const_shared_buffer>;
77+
78+
// utility function for casting @c wait_queue.data()
79+
template <class C>
80+
const char* cast_to_char_ptr (const C buf) {
81+
return static_cast<const char*> (static_cast<const void*> (buf.data()));
82+
}
83+
84+
// generate 20 random numbers in a centile based on the 'start number' passed to
85+
// the @c Device::processData method.
86+
// Place numbers into a shared @c chops::wait_queue
87+
class DeviceDataGenerator {
88+
private:
89+
constexpr static int INTERVAL = 20; // usec: how often to generate a random #
90+
constexpr static int NUM_LIMIT = 20; // how many numbers to generate
91+
92+
device_q_t& m_device_q;
93+
std::atomic<int>& m_num_device_threads;
94+
const int m_start_num;
95+
96+
public:
97+
constexpr DeviceDataGenerator(device_q_t& wait, std::atomic<int>& dev_threads,
98+
const int start_num) : m_device_q(wait), m_num_device_threads(dev_threads),
99+
m_start_num(start_num) {};
100+
101+
// generate a random number, range [(0...99) + start_num * 100]
102+
// every INTERVAL usec
103+
void operator()() {
104+
std::srand(static_cast<unsigned int>(std::time(nullptr)));
105+
int num_count = NUM_LIMIT;
106+
// create NUM_LIMIT random numbers
107+
while (num_count-- > 0) {
108+
// create random number every INTERVAL usec
109+
std::this_thread::sleep_for(std::chrono::milliseconds(INTERVAL));
110+
const int val = (std::rand() % 100) + (m_start_num * 100);
111+
// put it in the wait_queue
112+
m_device_q.push(val);
113+
}
114+
115+
// cleanup: decrement atomic number of device threads
116+
m_num_device_threads.fetch_sub(1);
117+
}
118+
};
119+
120+
// Read numbers in m_device_q, sort by centile into @c std::vector<vector<int>>.
121+
// When 5 numbers accumulated in a centile, create string to place
122+
// into data_q, then empty that vector.
123+
class DataProcessor {
124+
private:
125+
device_q_t& m_device_q;
126+
data_q_t& m_data_q;
127+
std::atomic<int>& m_num_device_threads;
128+
std::atomic<int>& m_num_data_threads;
129+
const int m_num_devices;
130+
131+
std::vector<std::vector<int> > m_store;
132+
133+
public:
134+
DataProcessor(device_q_t& devq, data_q_t& datq,
135+
std::atomic<int>& device_threads, std::atomic<int>& data_threads, const int num_devices) :
136+
m_device_q(devq), m_data_q(datq), m_num_device_threads(device_threads),
137+
m_num_data_threads(data_threads), m_num_devices(num_devices) {
138+
139+
// initialize vector
140+
for (int i = 0; i < m_num_devices; ++i) {
141+
m_store.push_back(std::vector<int>());
142+
}
143+
};
144+
145+
// read data from m_device_q (wait_queue), periodically send to m_data_q
146+
void operator()() {
147+
// read from the m_device_q while DeviceDataGenerator threads active
148+
while (m_num_device_threads.load() > 0 ) {
149+
readData();
150+
}
151+
152+
// finish up - read until m_device_q is empty
153+
while (!m_device_q.empty()) {
154+
readData();
155+
}
156+
157+
// make sure nothing left in m_store vector<int>
158+
cleanup();
159+
160+
// exit: subtract 1 from atomic data processor thread count
161+
m_num_data_threads.fetch_sub(1);
162+
}
163+
164+
private:
165+
// read number from m_device_q, if any present, place into m_store vector
166+
// by index number. When 5 numbers are in a centile vector, call
167+
// formatData()
168+
void readData() {
169+
170+
if (m_device_q.empty()) {
171+
return;
172+
}
173+
174+
std::optional<int> result = m_device_q.try_pop();
175+
if (!result) {
176+
return;
177+
}
178+
179+
const int val = result.value();
180+
const int index = val / 100;
181+
182+
// insert into proper vector
183+
m_store.at(index).push_back(val);
184+
// if 5 elements, create string to send to DB
185+
if (m_store.at(index).size() >= 5) {
186+
formatData(index);
187+
}
188+
}
189+
190+
// create a string to send to the DB thread via m_data_q
191+
void formatData(int index) {
192+
// first number in s is the index
193+
std::string s = std::to_string(index) + " ";
194+
// convert each int into a string, and append to s
195+
// add spaces for formatting
196+
// first number is the 'index' or centile
197+
// example: 8 870 813 808 827 874
198+
auto f = [&](int num) {
199+
s += (index == 0 ? " " : "");
200+
s += (num / 10 == 0 ? " " : "");
201+
s += (index < 10 ? " " : "");
202+
s += std::to_string(num) + " ";
203+
};
204+
std::for_each(m_store.at(index).begin(), m_store.at(index).end(), f);
205+
// clear that vector
206+
m_store.at(index).clear();
207+
// put the sting into m_data_q, using API for @d std::const_shared_queue
208+
m_data_q.emplace_push(s.c_str(), s.size() + 1);
209+
}
210+
211+
// process any numbers left in m_store vector<int>
212+
void cleanup() {
213+
for (int index = 0; index < static_cast<int> (m_store.size()); index++) {
214+
if (!m_store.at(index).empty()) {
215+
formatData(index);
216+
}
217+
}
218+
}
219+
};
220+
221+
// (very) simple database that reads string from m_data_q and places into proper centile
222+
// generate 'Data Report' on exit
223+
class Database {
224+
private:
225+
data_q_t& m_data_q;
226+
std::atomic<int>& m_num_data_threads;
227+
std::promise<std::string>& m_promise;
228+
const unsigned int m_num_devices;
229+
230+
std::vector<std::string> db;
231+
232+
public:
233+
Database(data_q_t& datq, std::atomic<int>& data_threads,
234+
std::promise<std::string>& promise, int num_devices) : m_data_q(datq),
235+
m_num_data_threads(data_threads), m_promise(promise), m_num_devices(num_devices) {
236+
// initialize vector<vector<string>>
237+
for (int i = 0; i < static_cast<int> (m_num_devices); i++) {
238+
std::string str = "[" + std::to_string(i) + "]\t";
239+
db.push_back(str);
240+
}
241+
};
242+
243+
// read data from m_data_q
244+
void operator()() {
245+
// read data while data_threads active
246+
while (m_num_data_threads.load() > 0) {
247+
// loop back if empty
248+
if (m_data_q.empty()) {
249+
continue;
250+
}
251+
processData();
252+
}
253+
254+
// after all data_threads closed, read any data left in m_data_q
255+
while (!m_data_q.empty()) {
256+
processData();
257+
}
258+
259+
// database final report
260+
createReport();
261+
}
262+
263+
private:
264+
// extract string from m_data_q, append to db vector
265+
void processData() {
266+
// get string from m_data_q
267+
std::optional<chops::const_shared_buffer> buffer = m_data_q.try_pop();
268+
if (!buffer) {
269+
return;
270+
}
271+
272+
const std::string str = cast_to_char_ptr (buffer.value());
273+
// get the index
274+
const int index = std::stoi(str.substr(0, str.find_first_of(" ")));
275+
// append to proper vector
276+
db.at(index) += str.substr(str.find_first_of(" ") + 1,
277+
std::string::npos);
278+
}
279+
280+
// create data report, place into promise object for printing
281+
void createReport() const {
282+
std::cout << "\nData Report" << std::endl;
283+
// create report string
284+
std::string s_out;
285+
std::for_each(db.begin(), db.end(),
286+
[&] (const std::string s) { s_out += s + "\n"; });
287+
// place into promise object
288+
m_promise.set_value(s_out);
289+
290+
}
291+
};
292+
293+
// thread creation and management
294+
class ThreadManagement {
295+
private:
296+
const int m_num_devices;
297+
const int m_num_data_proc;
298+
299+
public:
300+
constexpr ThreadManagement(const int num_devices, const int num_data_proc) :
301+
m_num_devices(num_devices), m_num_data_proc(num_data_proc) {};
302+
303+
void operator()() {
304+
// shared
305+
device_q_t device_q;
306+
data_q_t data_q;
307+
std::atomic<int> num_device_threads(m_num_devices);
308+
std::atomic<int> num_data_threads(m_num_data_proc);
309+
std::promise<std::string> promise_obj;
310+
// local
311+
std::future<std::string> future_obj = promise_obj.get_future();
312+
std::vector<std::thread> threadListDevice;
313+
std::vector<std::thread> threadListData;
314+
315+
// create threads
316+
// DeviceDataGenerator
317+
for (int i = 0; i < static_cast<int> (m_num_devices); i++) {
318+
threadListDevice.push_back(std::thread(DeviceDataGenerator(device_q,
319+
num_device_threads, i)));
320+
}
321+
// DataProcessor
322+
for (int i = 0; i < m_num_data_proc; i++) {
323+
threadListData.push_back(std::thread(DataProcessor(device_q, data_q,
324+
num_device_threads, num_data_threads, m_num_devices)));
325+
}
326+
327+
// Database
328+
std::thread dbThread(Database(data_q, num_data_threads, promise_obj, m_num_devices));
329+
330+
// join threads
331+
std::for_each(threadListDevice.begin(), threadListDevice.end(),
332+
std::mem_fn(&std::thread::join));
333+
std::for_each(threadListData.begin(), threadListData.end(),
334+
std::mem_fn(&std::thread::join));
335+
dbThread.join();
336+
337+
// print Database report
338+
std::cout << future_obj.get() << std::endl;
339+
}
340+
};
341+
342+
343+
/* constants */
344+
// number of DeviceDataGenerator threads
345+
constexpr static unsigned int NUM_DEVICES = 20; // must be > 0
346+
// number of DataProcessor threads
347+
constexpr static unsigned int NUM_DATA_PROC = 5; // must be > 0
348+
349+
int main(int argc, char* argv[]) {
350+
int num_devices = NUM_DEVICES;
351+
int num_data_proc = NUM_DATA_PROC;
352+
353+
if (argc != 1 && argc != 3) {
354+
std::cout << "usage: \n";
355+
std::cout << " 0 parameters: (default values)\n";
356+
std::cout << " 2 parameters: <number of devices>, ";
357+
std::cout << "<number of data processors> \n";
358+
359+
return EXIT_FAILURE;
360+
}
361+
362+
if (argc == 3) {
363+
num_devices = std::atoi(argv[1]);
364+
num_data_proc = std::atoi(argv[2]);
365+
}
366+
367+
std::cout << "DeviceDataGenerator threads: " << num_devices << std::endl;
368+
std::cout << "DataProcessor threads: " << num_data_proc << std::endl;
369+
std::cout << "Database threads: " << 1 << std::endl << std::endl;
370+
371+
std::cout << "Processing data...\n";
372+
373+
ThreadManagement(num_devices, num_data_proc)();
374+
375+
return EXIT_SUCCESS;
376+
}

0 commit comments

Comments
 (0)