Newer
Older
/**
* @file handle.hpp
* @copyright Copyright (c) 2020 University of Turku, MIT License
* @author Nicolas Pope
*/
#pragma once
#include <functional>
#include <unordered_map>
#include <ftl/threads.hpp>
#include <ftl/exception.hpp>
namespace ftl {
struct Handle;
struct BaseHandler {
virtual void removeUnsafe(const Handle &) = 0;
inline Handle make_handle(BaseHandler*, int);
protected:
std::mutex mutex_;
int id_ = 0;
};
/**
* A `Handle` is used to manage registered callbacks, allowing them to be
* removed safely whenever the `Handle` instance is destroyed.
*/
struct [[nodiscard]] Handle {
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
friend struct BaseHandler;
/**
* Cancel the callback and invalidate the handle.
*/
inline void cancel() {
if (handler_) {
handler_->remove(*this);
}
handler_ = nullptr;
}
inline void innerCancel() {
if (handler_) {
handler_->removeUnsafe(*this);
}
handler_ = nullptr;
}
inline int id() const { return id_; }
Handle() : handler_(nullptr), id_(0) {}
Handle(const Handle &) = delete;
Handle &operator=(const Handle &) = delete;
inline Handle(Handle &&h) : handler_(nullptr) {
if (handler_) handler_->remove(*this);
handler_ = h.handler_;
h.handler_ = nullptr;
id_ = h.id_;
}
inline Handle &operator=(Handle &&h) {
if (handler_) handler_->remove(*this);
handler_ = h.handler_;
h.handler_ = nullptr;
id_ = h.id_;
return *this;
}
inline ~Handle() {
if (handler_) {
handler_->remove(*this);
}
}
private:
BaseHandler *handler_;
int id_;
Handle(BaseHandler *h, int id) : handler_(h), id_(id) {}
};
/**
* This class is used to manage callbacks. The template parameters are the
* arguments to be passed to the callback when triggered. This class is already
* thread-safe.
*
* POSSIBLE BUG: On destruction any remaining handles will be left with
* dangling pointer to Handler.
*/
template <typename ...ARGS>
struct Handler : BaseHandler {
// Ensure all thread pool jobs are done
while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
/**
* Add a new callback function. It returns a `Handle` object that must
* remain in scope, the destructor of the `Handle` will remove the callback.
*/
Handle on(const std::function<bool(ARGS...)> &f) {
std::unique_lock<std::mutex> lk(mutex_);
int id = id_++;
callbacks_[id] = f;
return make_handle(this, id);
}
/**
* Safely trigger all callbacks. Note that `Handler` is locked when
* triggering so callbacks cannot make modifications to it or they will
* lock up. To remove a callback, return false from the callback, else
* return true.
*/
void trigger(ARGS ...args) {
bool hadFault = false;
std::unique_lock<std::mutex> lk(mutex_);
for (auto i = callbacks_.begin(); i != callbacks_.end(); ) {
bool keep = true;
try {
keep = i->second(args...);
}
if (!keep) i = callbacks_.erase(i);
else
++i;
}
if (hadFault) throw FTL_Error("Callback exception: " << faultMsg);
}
/**
* Call all the callbacks in another thread. The callbacks are done in a
* single thread, not in parallel.
*/
void triggerAsync(ARGS ...args) {
++jobs_;
ftl::pool.push([this, args...](int id) {
bool hadFault = false;
std::unique_lock<std::mutex> lk(mutex_);
for (auto i = callbacks_.begin(); i != callbacks_.end(); ) {
bool keep = true;
try {
keep = i->second(args...);
}
if (!keep) i = callbacks_.erase(i);
else
++i;
}
--jobs_;
if (hadFault) throw FTL_Error("Callback exception: " << faultMsg);
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
});
}
/**
* Each callback is called in its own thread job. Note: the return value
* of the callback is ignored in this case and does not allow callback
* removal via the return value.
*/
void triggerParallel(ARGS ...args) {
std::unique_lock<std::mutex> lk(mutex_);
jobs_ += callbacks_.size();
for (auto i = callbacks_.begin(); i != callbacks_.end(); ++i) {
ftl::pool.push([this, f = i->second, args...](int id) {
try {
f(args...);
} catch (const ftl::exception &e) {
--jobs_;
throw e;
}
--jobs_;
});
}
}
/**
* Remove a callback using its `Handle`. This is equivalent to allowing the
* `Handle` to be destroyed or cancelled.
*/
void remove(const Handle &h) override {
{
std::unique_lock<std::mutex> lk(mutex_);
callbacks_.erase(h.id());
}
// Make sure any possible call to removed callback has finished.
while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
void removeUnsafe(const Handle &h) override {
callbacks_.erase(h.id());
// Make sure any possible call to removed callback has finished.
while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
void clear() {
callbacks_.clear();
}
private:
std::unordered_map<int, std::function<bool(ARGS...)>> callbacks_;
std::atomic_int jobs_ = 0;
};
/**
* This class is used to manage callbacks. The template parameters are the
* arguments to be passed to the callback when triggered. This class is already
* thread-safe. Note that this version only allows a single callback at a time
* and throws an exception if multiple are added without resetting.
*/
template <typename ...ARGS>
struct SingletonHandler : BaseHandler {
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
/**
* Add a new callback function. It returns a `Handle` object that must
* remain in scope, the destructor of the `Handle` will remove the callback.
*/
[[nodiscard]] Handle on(const std::function<bool(ARGS...)> &f) {
std::unique_lock<std::mutex> lk(mutex_);
if (callback_) throw FTL_Error("Callback already bound");
callback_ = f;
return make_handle(this, id_++);
}
/**
* Safely trigger all callbacks. Note that `Handler` is locked when
* triggering so callbacks cannot make modifications to it or they will
* lock up. To remove a callback, return false from the callback, else
* return true.
*/
bool trigger(ARGS ...args) {
std::unique_lock<std::mutex> lk(mutex_);
if (callback_) {
bool keep = callback_(std::forward<ARGS>(args)...);
if (!keep) callback_ = nullptr;
return keep;
} else {
return false;
}
}
/**
* Remove a callback using its `Handle`. This is equivalent to allowing the
* `Handle` to be destroyed or cancelled. If the handle does not match the
* currently bound callback then the callback is not removed.
*/
void remove(const Handle &h) override {
std::unique_lock<std::mutex> lk(mutex_);
if (h.id() == id_-1) callback_ = nullptr;
}
void removeUnsafe(const Handle &h) override {
if (h.id() == id_-1) callback_ = nullptr;
}
void reset() { callback_ = nullptr; }
operator bool() const { return static_cast<bool>(callback_); }
private:
std::function<bool(ARGS...)> callback_;
ftl::Handle ftl::BaseHandler::make_handle(BaseHandler *h, int id) {