libabigail
Loading...
Searching...
No Matches
abg-workers.cc
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2// -*- Mode: C++ -*-
3//
4// Copyright (C) 2013-2026 Red Hat, Inc.
5//
6// Author: Dodji Seketeli
7
8/// @file
9///
10/// This file implements the worker threads (or thread pool) design
11/// pattern. It aims at performing a set of tasks in parallel, using
12/// the multi-threading capabilities of the underlying processor(s).
13
14#include <assert.h>
15#include <unistd.h>
16#include <queue>
17#include <vector>
18#include <iostream>
19#include <atomic>
20#include <thread>
21#include <mutex>
22#include <condition_variable>
23
24#include "abg-fwd.h"
25#include "abg-internal.h"
26// <headers defining libabigail's API go under here>
27ABG_BEGIN_EXPORT_DECLARATIONS
28
29#include "abg-workers.h"
30
31ABG_END_EXPORT_DECLARATIONS
32// </headers defining libabigail's API>
33
34namespace abigail
35{
36
37namespace workers
38{
39
40using std::mutex;
41using std::unique_lock;
42using std::lock_guard;
43using std::condition_variable;
44using std::vector;
45
46/// @defgroup thread_pool Worker Threads
47/// @{
48///
49/// \brief Libabigail's implementation of Thread Pools.
50///
51/// The main interface of this pattern is a @ref queue of @ref tasks
52/// to be performed. Associated to that queue are a set of worker
53/// threads (these are native posix threads) that sits there, idle,
54/// until at least one @ref task is added to the queue.
55///
56/// When a @ref task is added to the @ref queue, one thread is woken
57/// up, picks the @ref task, removes it from the @ref queue, and
58/// executes the instructions it carries. We say the worker thread
59/// performs the @ref task.
60///
61/// When the worker thread is done performing the @ref task, the
62/// performed @ref task is added to another queue, named as the "done
63/// queue". Then the thread looks at the @ref queue of tasks to be
64/// performed again, and if there is at least one task in that queue,
65/// the same process as above is done. Otherwise, the thread blocks,
66/// waiting for a new task to be added to the queue.
67///
68/// By default, the number of worker threads is equal to the number of
69/// execution threads advertised by the underlying processor.
70///
71/// Note that the user of the queue can either wait for all the tasks
72/// to be performed by the pool of threads,and them stop them, get the
73/// vector of done tasks and proceed to whatever computation she may
74/// need next.
75///
76/// Or she can choose to be asynchronously notified whenever a task is
77/// performed and added to the "done queue".
78///
79///@}
80
81/// @return The number of hardware threads of executions advertised by
82/// the underlying processor.
83size_t
85{return std::thread::hardware_concurrency();}
86
87/// @return The number of hardware threads of executions advertised by
88/// the underlying processor. If libabigail has been configured
89/// without multithreading support, this returns 1.
90///
91/// @return the number of hardware threads of executions advertised by
92/// the underlying processor *if* libabigail has been configured with
93/// multithreading support.
94size_t
96{
97 #ifdef HAVE_MULTITHREADING_SUPPORT
98 return get_number_of_threads();
99#else
100 return 1;
101#endif
102}
103
104/// The abstraction of a worker thread.
105///
106/// This is an implementation detail of the @ref queue public
107/// interface type of this worker thread design pattern.
108struct worker
109{
110 std::shared_ptr<std::thread> thread;
111
112 worker()
113 {}
114
115 static queue::priv*
116 wait_to_execute_a_task(queue::priv*);
117}; // end struct worker
118
119// </worker declarations>
120
121// <queue stuff>
122
123/// The private data structure of the task queue.
124struct queue::priv
125{
126 // An atomic boolean to say if the user wants to shutdown the worker
127 // threads.
128 std::atomic<bool> bring_workers_down;
129 // The number of worker threads.
130 size_t num_workers = 0;
131 // A mutex that protects the todo tasks queue from being accessed in
132 // read/write by two threads at the same time.
133 mutex tasks_todo_mutex;
134 // The queue condition variable. This condition is used to make the
135 // worker threads sleep until a new task is added to the queue of
136 // todo tasks. Whenever a new task is added to that queue, a signal
137 // is sent to all a thread sleeping on this condition variable.
138 condition_variable tasks_todo_cond;
139 // A mutex that protects the done tasks queue from being accessed in
140 // read/write by two threads at the same time.
141 mutex tasks_done_mutex;
142 // The queue of staged tasks;
143 std::queue<task_sptr> tasks_staged;
144 mutex tasks_staged_mutex;
145 // The todo task queue itself.
146 std::queue<task_sptr> tasks_todo;
147 // The done task queue itself.
148 vector<task_sptr> tasks_done;
149 // This functor is invoked to notify the user of this queue that a
150 // task has been completed and has been added to the done tasks
151 // vector. We call it a notifier. This notifier is the default
152 // notifier of the work queue; the one that is used when the user
153 // has specified no notifier. It basically does nothing.
154 static task_done_notify default_notify;
155 // This is a reference to the the notifier that is actually used in
156 // the queue. It's either the one specified by the user or the
157 // default one.
158 task_done_notify& notify;
159 // A vector of the worker threads.
160 vector<std::shared_ptr<worker>>workers;
161
162 /// A constructor of @ref queue::priv.
163 ///
164 /// @param nb_workers the number of worker threads to have in the
165 /// thread pool.
166 ///
167 /// @param task_done_notify a functor object that is invoked by the
168 /// worker thread which has performed the task, right after it's
169 /// added that task to the vector of the done tasks.
170 priv(size_t nb_workers = get_number_of_threads(),
171 task_done_notify& n = default_notify)
172 : bring_workers_down(false),
173 num_workers(nb_workers),
174 notify(n)
175 {create_workers();}
176
177 /// Test without data race if the tasks TODO queue is empty.
178 ///
179 /// @return true iff the tasks TODO queue is empty.
180 bool
181 tasks_todo_queue_is_empty()
182 {
183 lock_guard<mutex> lock(tasks_todo_mutex);
184 return tasks_todo.empty();
185 }
186
187 /// Create the worker threads pool and have all threads sit idle,
188 /// waiting for a task to be added to the todo queue.
189 void
190 create_workers()
191 {
192 for (unsigned i = 0; i < num_workers; ++i)
193 {
194 std::shared_ptr<worker> w(new worker);
195 w->thread.reset(new std::thread(&worker::wait_to_execute_a_task,
196 this));
197 workers.push_back(w);
198 }
199 }
200
201 /// Submit a task to the queue of tasks to be performed.
202 ///
203 /// This wakes up one thread from the pool which immediatly starts
204 /// performing the task. When it's done with the task, it goes back
205 /// to be suspended, waiting for a new task to be scheduled.
206 ///
207 /// @param t the task to schedule. Note that a nil task won't be
208 /// scheduled. If the queue is empty, the task @p t won't be
209 /// scheduled either.
210 ///
211 /// @return true iff the task @p t was successfully scheduled.
212 bool
213 schedule_task(const task_sptr& t)
214 {
215 if (workers.empty() || !t)
216 return false;
217
218 {
219 unique_lock<mutex> lock(tasks_todo_mutex);
220 if (bring_workers_down)
221 // We were asked to bring the workers down so we shouldn't be
222 // scheduling a new task.
223 return false;
224
225 tasks_todo.push(t);
226 tasks_todo_cond.notify_one();
227 }
228
229 return true;
230 }
231
232 /// Submit a vector of task to the queue of tasks to be performed.
233 ///
234 /// This wakes up threads of the pool which immediatly start
235 /// performing the tasks. When they are done with the task, they go
236 /// back to be suspended, waiting for new tasks to be scheduled.
237 ///
238 /// @param tasks the tasks to schedule.
239 bool
240 schedule_tasks(const tasks_type& tasks)
241 {
242 bool is_ok= true;
243 for (tasks_type::const_iterator t = tasks.begin(); t != tasks.end(); ++t)
244 is_ok &= schedule_task(*t);
245 return is_ok;
246 }
247
248 /// Stages a task to be scheduled later.
249 ///
250 /// Unlike @ref schedule_task(), this function does *NOT* starts the
251 /// execution of the task.
252 ///
253 /// The staged task is put into a FIFO queue until @ref
254 /// schedule_staged_tasks() later schedules them all for execution.
255 ///
256 /// @param task the task to stage.
257 ///
258 /// @return true iff the task could be scheduled.
259 bool
260 stage_task(const task_sptr& task)
261 {
262 unique_lock<mutex> lock(tasks_staged_mutex);
263 tasks_staged.push(task);
264 return true;
265 }
266
267 /// Schedule the tasks that have been previously staged by
268 /// stage_task().
269 void
270 schedule_staged_tasks()
271 {
272 unique_lock<mutex> lock(tasks_staged_mutex);
273 while (!tasks_staged.empty())
274 {
275 task_sptr t = tasks_staged.front();
276 tasks_staged.pop();
277 schedule_task(t);
278 }
279 }
280
281 /// Signal all the threads (of the pool) which are suspended and
282 /// waiting to perform a task, so that they wake up and end up their
283 /// execution. If there is no task to perform, they just end their
284 /// execution. If there are tasks to perform, they finish them and
285 /// then end their execution.
286 ///
287 /// This function then joins all the tasks of the pool, waiting for
288 /// them to finish, and then it returns. In other words, this
289 /// function suspends the thread of the caller, waiting for the
290 /// worker threads to finish their tasks, and end their execution.
291 ///
292 /// If the user code wants to work with the thread pool again,
293 /// she'll need to create them again, using the member function
294 /// create_workers().
295 void
296 do_bring_workers_down()
297 {
298 if (workers.empty())
299 return;
300
301 // Signal the workers that we want them down, wake them all up,
302 // let them finish their final task before termination and let
303 // them terminate.
304 {
305 unique_lock<mutex> lock(tasks_todo_mutex);
306 bring_workers_down = true;
307 tasks_todo_cond.notify_all();
308 }
309
310 for (auto& worker : workers)
311 worker->thread->join();
312
313 workers.clear();
314 }
315
316 /// Destructors of @ref queue::priv type.
317 ~priv()
318 {do_bring_workers_down();}
319
320}; //end struct queue::priv
321
322// default initialize the default notifier.
323queue::task_done_notify queue::priv::default_notify;
324
325/// Default constructor of the @ref queue type.
326///
327/// By default the queue is created with a number of worker threaders
328/// which is equals to the number of simultaneous execution threads
329/// supported by the underlying processor.
331 : p_(new priv())
332{}
333
334/// Constructor of the @ref queue type.
335///
336/// @param number_of_workers the number of worker threads to have in
337/// the pool.
338queue::queue(unsigned number_of_workers)
339 : p_(new priv(number_of_workers))
340{}
341
342/// Constructor of the @ref queue type.
343///
344/// @param number_of_workers the number of worker threads to have in
345/// the pool.
346///
347/// @param the notifier to invoke when a task is done doing its job.
348/// Users should create a type that inherit this @ref task_done_notify
349/// class and overload its virtual task_done_notify::operator()
350/// operator function. Note that the code of that
351/// task_done_notify::operator() is assured to run in *sequence*, with
352/// respect to the code of other task_done_notify::operator() from
353/// other tasks.
354queue::queue(unsigned number_of_workers,
355 task_done_notify& notifier)
356 : p_(new priv(number_of_workers, notifier))
357{}
358
359/// Getter of the size of the queue. This gives the number of task
360/// still present in the queue.
361///
362/// @return the number of task still present in the queue.
363size_t
365{return p_->tasks_todo.size();}
366
367/// Submit a task to the queue of tasks to be performed.
368///
369/// This wakes up one thread from the pool which immediatly starts
370/// performing the task. When it's done with the task, it goes back
371/// to be suspended, waiting for a new task to be scheduled.
372///
373/// @param t the task to schedule. Note that if the queue is empty or
374/// if the task is nil, the task is not scheduled.
375///
376/// @return true iff the task was successfully scheduled.
377bool
378queue::schedule_task(const task_sptr& t)
379{return p_->schedule_task(t);}
380
381/// Submit a vector of tasks to the queue of tasks to be performed.
382///
383/// This wakes up one or more threads from the pool which immediatly
384/// start performing the tasks. When the threads are done with the
385/// tasks, they goes back to be suspended, waiting for a new task to
386/// be scheduled.
387///
388/// @param tasks the tasks to schedule.
389bool
391{return p_->schedule_tasks(tasks);}
392
393/// Stages a task to be scheduled later.
394///
395/// Unlike @ref schedule_task(), this function does *NOT* starts the
396/// execution of the task.
397///
398/// The staged task is put into a FIFO queue until @ref
399/// schedule_staged_tasks() later schedules them all for execution.
400///
401/// @param task the task to stage.
402///
403/// @return true iff the task could be scheduled.
404bool
405queue::stage_task(const task_sptr& task)
406{return p_->stage_task(task);}
407
408/// Schedule the tasks that have been previously staged by
409/// stage_task().
410void
412{p_->schedule_staged_tasks();}
413
414/// Suspends the current thread until all worker threads finish
415/// performing the tasks they are executing.
416///
417/// If the worker threads were suspended waiting for a new task to
418/// perform, they are woken up and their execution ends.
419///
420/// The execution of the current thread is resumed when all the
421/// threads of the pool have finished their execution and are
422/// terminated.
423void
425{p_->do_bring_workers_down();}
426
427/// Getter of the vector of tasks that got performed.
428///
429/// @return the vector of tasks that got performed.
430vector<task_sptr>&
432{return p_->tasks_done;}
433
434/// Destructor for the @ref queue type.
437
438/// The default function invocation operator of the @ref queue type.
439///
440/// This does nothing.
441void
442queue::task_done_notify::operator()(const task_sptr&/*task_done*/)
443{
444}
445
446// </queue stuff>
447
448// <worker definitions>
449
450/// Wait to be woken up by a thread condition signal, then look if
451/// there is a task to be executed. If there is, then pick one (in a
452/// FIFO manner), execute it, and put the executed task into the set
453/// of done tasks.
454///
455/// @param p the private data of the "task queue" type to consider.
456///
457/// @param return the same private data of the task queue type we got
458/// in argument.
459queue::priv*
460worker::wait_to_execute_a_task(queue::priv* p)
461{
462 while (true)
463 {
464 task_sptr t;
465 {
466 unique_lock<mutex> lock(p->tasks_todo_mutex);
467
468 // If there is no more tasks to perform and the queue is not to
469 // be brought down then wait (sleep) for new tasks to come up.
470 while (p->tasks_todo.empty() && !p->bring_workers_down)
471 p->tasks_todo_cond.wait(lock);
472
473 // We were woken up. So maybe there are tasks to perform? If
474 // so, get a task from the queue ...
475 if (!p->tasks_todo.empty())
476 {
477 t = p->tasks_todo.front();
478 p->tasks_todo.pop();
479 }
480 }
481
482 // If we've got a task to perform then perform it and when it's
483 // done then add to the set of tasks that are done.
484 if (t)
485 {
486 t->perform();
487
488 // Add the task to the vector of tasks that are done and
489 // notify listeners about the fact that the task is done.
490 //
491 // Note that this (including the notification) is not
492 // happening in parallel. So the code performed by the
493 // notifier during the notification is running sequentially,
494 // not in parallel with any other task that was just done
495 // and that is notifying its listeners.
496 {
497 lock_guard<mutex> lock(p->tasks_done_mutex);
498 p->tasks_done.push_back(t);
499 p->notify(t);
500 }
501 }
502
503 // ensure we access bring_workers_down always guarded
504 if (p->bring_workers_down && p->tasks_todo_queue_is_empty())
505 break;
506 }
507
508 return p;
509}
510// </worker definitions>
511} //end namespace workers
512} //end namespace abigail
This file declares an interface for the worker threads (or thread pool) design pattern....
~queue()
Destructor for the queue type.
tasks_type & get_completed_tasks() const
Getter of the vector of tasks that got performed.
void wait_for_workers_to_complete()
Suspends the current thread until all worker threads finish performing the tasks they are executing.
void schedule_staged_tasks()
Schedule the tasks that have been previously staged by stage_task().
bool stage_task(const task_sptr &)
Stages a task to be scheduled later.
std::vector< task_sptr > tasks_type
A convenience typedef for a vector of task_sptr.
size_t get_size() const
Getter of the size of the queue. This gives the number of task still present in the queue.
bool schedule_tasks(const tasks_type &)
Submit a vector of tasks to the queue of tasks to be performed.
queue()
Default constructor of the queue type.
bool schedule_task(const task_sptr &)
Submit a task to the queue of tasks to be performed.
This represents a task to be performed.
Definition abg-workers.h:48
size_t get_number_of_threads()
size_t get_number_of_available_threads()
Toplevel namespace for libabigail.
This functor is to notify listeners that a given task scheduled for execution has been fully executed...
virtual void operator()(const task_sptr &task_done)
The default function invocation operator of the queue type.