Skip to content

Commit c6ef858

Browse files
Moved threaded_queue_buffer_demo here, needs to be worked on
1 parent 0493870 commit c6ef858

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)